Redis 持久化策略淺析

語言: CN / TW / HK

動手點關注

乾貨不迷路

Redis(Remote Dictionary Server ),即遠端字典服務,是一個開源的記憶體快取記憶體資料儲存服務。使用 ANSI C 語言編寫,支援網路、可基於記憶體亦可持久化的日誌型、Key-Value 資料儲存,並提供多種語言的 API。

▶簡介

Redis 是記憶體資料庫,資料都是儲存在記憶體中,為了避免程序退出導致資料的永久丟失,需要定期將 Redis 中的資料以某種形式(資料或命令)從記憶體儲存到硬碟。當下次 Redis 重啟時,利用持久化檔案實現資料恢復。除此之外,為了進行災難備份,可以將持久化檔案拷貝到一個遠端位置。Redis 的持久化機制有兩種:

  • RDB(Redis Data Base) 記憶體快照

  • AOF(Append Only File) 增量日誌

RDB 將當前資料儲存到硬碟,AOF 則是將每次執行的寫命令儲存到硬碟(類似於 MySQL 的 Binlog)。AOF 持久化的實時性更好,即當程序意外退出時丟失的資料更少。

▶RDB持久化

簡介

RDB ( Redis Data Base) 指的是在指定的時間間隔內將記憶體中的資料集快照寫入磁碟,RDB 是記憶體快照(記憶體資料的二進位制序列化形式)的方式持久化,每次都是從 Redis 中生成一個快照進行資料的全量備份。

優點:

  • 儲存緊湊,節省記憶體空間。

  • 恢復速度非常快。

  • 適合全量備份、全量複製的場景,經常用於災難恢復(對資料的完整性和一致性要求相對較低的場合)。

缺點:

  • 容易丟失資料,容易丟失兩次快照之間 Redis 伺服器中變化的資料。

  • RDB 通過 fork 子程序對記憶體快照進行全量備份,是一個重量級操作,頻繁執行成本高。

RDB 檔案結構

在預設情況下,Redis 將資料庫快照儲存在名字為 dump.rdb 的二進位制檔案中。RDB 檔案結構由五個部分組成:

(1)長度為5位元組的 REDIS 常量字串。

(2)4位元組的 db_version,標識 RDB 檔案版本。

(3)databases:不定長度,包含零個或多個數據庫,以及各資料庫中的鍵值對資料。

(4)1位元組的 EOF 常量,表示檔案正文內容結束。

(5)check_sum: 8位元組長的無符號整數,儲存校驗和。

資料結構舉例,以下是資料庫[0]和資料庫[3]有資料的情況:

RDB 檔案的建立

手動指令觸發

手動觸發 RDB 持久化的方式可以使用 save 命令和 bgsave 命令,這兩個命令的區別如下:

save :執行 save 指令,阻塞 Redis 的其他操作,會導致 Redis 無法響應客戶端請求,不建議使用。

bgsave :執行 bgsave 指令,Redis 後臺建立子程序,非同步進行快照的儲存操作,此時 Redis 仍然能響應客戶端的請求。

自動間隔性儲存

在預設情況下,Redis 將資料庫快照儲存在名字為 dump.rdb的二進位制檔案中。可以對 Redis 進行設定,讓它在“ N 秒內資料集至少有 M 個改動”這一條件被滿足時,自動儲存一次資料集。

比如說, 以下設定會讓 Redis 在滿足“ 60 秒內有至少有 10 個鍵被改動”這一條件時,自動儲存一次資料集: save 60 10

Redis 的預設配置如下,三個設定滿足其一即可觸發自動儲存:

save 60 10000
save 300 10
save 900 1

自動儲存配置的資料結構

記錄了伺服器觸發自動 BGSAVE 條件的 saveparams 屬性。

lastsave 屬性:記錄伺服器最後一次執行 SAVE 或者 BGSAVE 的時間。

dirty 屬性:以及自最後一次儲存 RDB 檔案以來,伺服器進行了多少次寫入。

備份過程

RDB 持久化方案進行備份時,Redis 會單獨 fork 一個子程序來進行持久化,會將資料寫入一個臨時檔案中,持久化完成後替換舊的 RDB 檔案。在整個持久化過程中,主程序(為客戶端提供服務的程序)不參與 IO 操作,這樣能確保 Redis 服務的高效能,RDB 持久化機制適合對資料完整性要求不高但追求高效恢復的使用場景。下面展示 RDB 持久化流程:

關鍵執行步驟如下

  1. Redis 父程序首先判斷:當前是否在執行 save,或 bgsave/bgrewriteaof 的子程序,如果在執行則 bgsave 命令直接返回。bgsave/bgrewriteaof 的子程序不能同時執行,主要是基於效能方面的考慮:兩個併發的子程序同時執行大量的磁碟寫操作,可能引起嚴重的效能問題。

  1. 父程序執行 fork 操作建立子程序,這個過程中父程序是阻塞的,Redis 不能執行來自客戶端的任何命令。父程序 fork 後,bgsave 命令返回”Background saving started”資訊並不再阻塞父程序,並可以響應其他命令。

  1. 子程序程序對記憶體資料生成快照檔案。

  1. 父程序在此期間接收的新的寫操作,使用 COW 機制寫入。

  1. 子程序完成快照寫入,替換舊 RDB 檔案,隨後子程序退出。

在生成 RDB 檔案的步驟中,在同步到磁碟和持續寫入這個過程是如何處理資料不一致的情況呢?生成快照 RDB 檔案時是否會對業務產生影響?

Fork 子程序的作用

上面說到了 RDB 持久化過程中,主程序會 fork 一個子程序來負責 RDB 的備份,這裡簡單介紹一下 fork:

  • Linux 作業系統中的程式,fork 會產生一個和父程序完全相同的子程序。子程序與父程序所有的資料均一致,但是子程序是一個全新的程序,與原程序是父子程序關係。

  • 出於效率考慮,Linux 作業系統中使用 COW(Copy On Write)寫時複製機制,fork 子程序一般情況下與父程序共同使用一段實體記憶體,只有在程序空間中的記憶體發生修改時,記憶體空間才會複製一份出來。

在 Redis 中,RDB 持久化就是充分的利用了這項技術,Redis 在持久化時呼叫 glibc 函式 fork 一個子程序,全權負責持久化工作,這樣父程序仍然能繼續給客戶端提供服務。fork 的子程序初始時與父程序(Redis 的主程序)共享同一塊記憶體;當持久化過程中,客戶端的請求對記憶體中的資料進行修改,此時就會通過 COW (Copy On Write) 機制對資料段頁面進行分離,也就是複製一塊記憶體出來給主程序去修改。

通過 fork 建立的子程序能夠獲得和父程序完全相同的記憶體空間,父程序對記憶體的修改對於子程序是不可見的,兩者不會相互影響;

通過 fork 建立子程序時不會立刻觸發大量記憶體的拷貝,採用的是寫時拷貝 COW (Copy On Write)。核心只為新生成的子程序建立虛擬空間結構,它們來複制於父程序的虛擬究竟結構,但是不為這些段分配實體記憶體,它們共享父程序的物理空間,當父子程序中有更改相應段的行為發生時,再為子程序相應的段分配物理空間;

▶AOF 持久化

簡介

AOF (Append Only File) 是把所有對記憶體進行修改的指令(寫操作)以獨立日誌檔案的方式進行記錄,重啟時通過執行 AOF 檔案中的 Redis 命令來恢復資料。類似MySql bin-log 原理。AOF 能夠解決資料持久化實時性問題,是現在 Redis 持久化機制中主流的持久化方案。

優點:

  • 資料的備份更加完整,丟失資料的概率更低,適合對資料完整性要求高的場景

  • 日誌檔案可讀,AOF 可操作性更強,可通過操作日誌檔案進行修復

缺點:

  • AOF 日誌記錄在長期執行中逐漸龐大,恢復起來非常耗時,需要定期對 AOF 日誌進行瘦身處理

  • 恢復備份速度比較慢

  • 同步寫操作頻繁會帶來效能壓力

AOF 檔案內容

被寫入 AOF 檔案的所有命令都是以 RESP 格式儲存的,是純文字格式儲存在 AOF 檔案中。

Redis 客戶端和服務端之間使用一種名為 RESP(REdis Serialization Protocol) 的二進位制安全文字協議進行通訊。

下面以一個簡單的 SET 命令進行舉例:

redis> SET mykey "hello"    //客戶端命令
OK

客戶端封裝為以下格式(每行用 \r\n 分隔)

*3
$3
SET
$5
mykey
$5
hello

AOF 檔案中記錄的文字內容如下

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n       //多出一個SELECT 0 命令,用於指定資料庫,為系統自動新增
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nhello\r\n

AOF 持久化實現

AOF 持久化方案進行備份時,客戶端所有請求的寫命令都會被追加到 AOF 緩衝區中,緩衝區中的資料會根據 Redis 配置檔案中配置的同步策略來同步到磁碟上的 AOF 檔案中,追加儲存每次寫的操作到檔案末尾。同時當 AOF 的檔案達到重寫策略配置的閾值時,Redis 會對 AOF 日誌檔案進行重寫,給 AOF 日誌檔案瘦身。Redis 服務重啟的時候,通過載入 AOF 日誌檔案來恢復資料。

AOF 的執行流程包括:

命令追加(append)

Redis 先將寫命令追加到緩衝區 aof_buf,而不是直接寫入檔案,主要是為了避免每次有寫命令都直接寫入硬碟,導致硬碟 IO 成為 Redis 負載的瓶頸。

struct redisServer {
//其他域...
sds aof_buf; // sds類似於Java中的String
//其他域...
}

檔案寫入(write)和檔案同步(sync)

根據不同的同步策略將 aof_buf 中的內容同步到硬碟;

Linux 作業系統中為了提升效能,使用了頁快取(page cache)。當我們將 aof_buf 的內容寫到磁碟上時,此時資料並沒有真正的落盤,而是在 page cache 中,為了將 page cache 中的資料真正落盤,需要執行 fsync / fdatasync 命令來強制刷盤。這邊的檔案同步做的就是刷盤操作,或者叫檔案刷盤可能更容易理解一些。

AOF 快取區的同步檔案策略由引數 appendfsync 控制,有三種同步策略,各個值的含義如下:

  • always :命令寫入 aof_buf 後立即呼叫系統 write 操作和系統 fsync 操作同步到 AOF 檔案,fsync 完成後執行緒返回。這種情況下,每次有寫命令都要同步到 AOF 檔案,硬碟 IO 成為效能瓶頸,Redis 只能支援大約幾百TPS寫入,嚴重降低了 Redis 的效能;即便是使用固態硬碟(SSD),每秒大約也只能處理幾萬個命令,而且會大大降低 SSD 的壽命。可靠性較高,資料基本不丟失。
  • no :命令寫入 aof_buf 後呼叫系統 write 操作,不對 AOF 檔案做 fsync 同步;同步由作業系統負責,通常同步週期為30秒。這種情況下,檔案同步的時間不可控,且緩衝區中堆積的資料會很多,資料安全性無法保證。
  • everysec :命令寫入 aof_buf 後呼叫系統 write 操作,write 完成後執行緒返回;fsync 同步檔案操作由專門的執行緒每秒呼叫一次。everysec 是前述兩種策略的折中,是效能和資料安全性的平衡,因此是 Redis 的預設配置,也是我們推薦的配置。

檔案重寫(rewrite)

定期重寫 AOF 檔案,達到壓縮的目的。

AOF 重寫是 AOF 持久化的一個機制,用來壓縮 AOF 檔案,通過 fork 一個子程序,重新寫一個新的 AOF 檔案,該次重寫不是讀取舊的 AOF 檔案進行復制,而是讀取記憶體中的Redis資料庫,重寫一份 AOF 檔案,有點類似於 RDB 的快照方式。

檔案重寫之所以能夠壓縮 AOF 檔案,原因在於:

  • 過期的資料不再寫入檔案

  • 無效的命令不再寫入檔案:如有些資料被重複設值(set mykey v1, set mykey v2)、有些資料被刪除了(sadd myset v1, del myset)等等

  • 多條命令可以合併為一個:如 sadd myset v1, sadd myset v2, sadd myset v3 可以合併為 sadd myset v1 v2 v3。不過為了防止單條命令過大造成客戶端緩衝區溢位,對於 list、set、hash、zset型別的 key,並不一定只使用一條命令;而是以某個常量為界將命令拆分為多條。這個常量在 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 中定義,不可更改,2.9版本中值是64。

AOF重寫

前面提到 AOF 的缺點時,說過 AOF 屬於日誌追加的形式來儲存 Redis 的寫指令,這會導致大量冗餘的指令儲存,從而使得 AOF 日誌檔案非常龐大,比如同一個 key 被寫了 10000 次,最後卻被刪除了,這種情況不僅佔記憶體,也會導致恢復的時候非常緩慢,因此 Redis 提供重寫機制來解決這個問題。Redis 的 AOF 持久化機制執行重寫後,儲存的只是恢復資料的最小指令集,我們如果想手動觸發可以使用如下指令:

bgrewriteaof

檔案重寫時機

相關引數:

  • aof_current_size:表示當前 AOF 檔案空間

  • aof_base_size:表示上一次重寫後 AOF 檔案空間

  • auto-aof-rewrite-min-size: 表示執行 AOF 重寫時檔案的最小體積,預設為64MB

  • auto-aof-rewrite-percentage: 表示當前 AOF 重寫時檔案空間(aof_current_size)超過上一次重寫後 AOF 檔案空間(aof_base_size)的比值多少後會重寫。

同時滿足下面兩個條件,則觸發 AOF 重寫機制:

  • aof_current_size 大於 auto-aof-rewrite-min-size

  • 當前 AOF 相比上一次 AOF 的增長率:(aof_current_size - aof_base_size)/aof_base_size 大於或等於 auto-aof-rewrite-percentage

AOF 重寫流程如下:

  1. bgrewriteaof 觸發重寫,判斷是否存在 bgsave 或者 bgrewriteaof 正在執行,存在則等待其執行結束再執行

  1. 主程序 fork 子程序,防止主程序阻塞無法提供服務,類似 RDB

  1. 子程序遍歷 Redis 記憶體快照中資料寫入臨時 AOF 檔案,同時會將新的寫指令寫入 aof_buf 和 aof_rewrite_buf 兩個重寫緩衝區,前者是為了寫回舊的 AOF 檔案,後者是為了後續重新整理到臨時 AOF 檔案中,防止快照記憶體遍歷時新的寫入操作丟失

  2. 子程序結束臨時 AOF 檔案寫入後,通知主程序

  3. 主程序會將上面 3 中的 aof_rewirte_buf 緩衝區中的資料寫入到子程序生成的臨時 AOF 檔案中

  4. 主程序使用臨時 AOF 檔案替換舊 AOF 檔案,完成整個重寫過程。

在實際中,為了避免在執行命令時造成客戶端輸入緩衝區溢位,重寫程式會檢查集合元素數量是否超過 REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,如果超過了,則會使用多個命令來記錄,而不單單使用一條命令。

Redis 2.9版本中該常量為64,如果一個命令的集合鍵包含超過了64個元素,重寫程式會拆成多個命令。

SADD <key> <elem1> <elem2>...<elem64>

SADD <key> <elem65> <elem66>...<elem128>

AOF重寫是一個有歧義的名字,該功能是通過直接讀取資料庫的鍵值對實現的,程式無需對現有AOF檔案進行任何讀入、分析或者寫入操作。

▶思考

AOF 與 WAL

Redis 為什麼考慮使用 AOF 而不是 WAL 呢?

很多資料庫都是採用的 Write Ahead Log(WAL)寫前日誌,其特點就是先把修改的資料記錄到日誌中,再進行寫資料的提交,可以方便通過日誌進行資料恢復。

但是 Redis 採用的卻是 AOF(Append Only File)寫後日志,特點就是先執行寫命令,把資料寫入記憶體中,再記錄日誌。

如果先讓系統執行命令,只有命令能執行成功,才會被記錄到日誌中。因此,Redis 使用寫後日志這種形式,可以避免出現記錄錯誤命令的情況。

另外還有一個原因就是:AOF 是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。

AOF 和 RDB 之間的相互作用

在版本號大於等於2.4的 Redis 中,BGSAVE 執行的過程中,不可以執行 BGREWRITEAOF。反過來說,在 BGREWRITEAOF 執行的過程中,也不可以執行 BGSAVE。這可以防止兩個 Redis 後臺程序同時對磁碟進行大量的 I/O 操作。

如果 BGSAVE 正在執行,並且使用者顯示地呼叫 BGREWRITEAOF 命令,那麼伺服器將向用戶回覆一個 OK 狀態,並告知使用者,BGREWRITEAOF 已經被預定執行:一旦 BGSAVE 執行完畢 BGREWRITEAOF 就會正式開始。

當 Redis 啟動時,如果 RDB 持久化和 AOF 持久化都被打開了,那麼程式會優先使用 AOF 檔案來恢復資料集,因為 AOF 檔案所儲存的資料通常是最完整的。

▶混合持久化

Redis4.0 後大部分的使用場景都不會單獨使用 RDB 或者 AOF 來做持久化機制,而是兼顧二者的優勢混合使用。其原因是 RDB 雖然快,但是會丟失比較多的資料,不能保證資料完整性;AOF 雖然能儘可能保證資料完整性,但是效能確實是一個詬病,比如重放恢復資料。

Redis從4.0版本開始引入 RDB-AOF 混合持久化模式,這種模式是基於 AOF 持久化模式構建而來的,混合持久化通過 aof-use-rdb-preamble yes 開啟。

那麼 Redis 伺服器在執行 AOF 重寫操作時,就會像執行 BGSAVE 命令那樣,根據資料庫當前的狀態生成出相應的 RDB 資料,並將這些資料寫入新建的 AOF 檔案中,至於那些在 AOF 重寫開始之後執行的 Redis 命令,則會繼續以協議文字的方式追加到新 AOF 檔案的末尾,即已有的 RDB 資料的後面。

換句話說,在開啟了 RDB-AOF 混合持久化功能之後,伺服器生成的 AOF 檔案將由兩個部分組成,其中位於 AOF 檔案開頭的是 RDB 格式的資料,而跟在 RDB 資料後面的則是 AOF 格式的資料。

當一個支援 RDB-AOF 混合持久化模式的 Redis 伺服器啟動並載入 AOF 檔案時,它會檢查 AOF 檔案的開頭是否包含了 RDB 格式的內容。

  • 如果包含,那麼伺服器就會先載入開頭的 RDB 資料,然後再載入之後的 AOF 資料。

  • 如果 AOF 檔案只包含 AOF 資料,那麼伺服器將直接載入 AOF 資料。

其日誌檔案結構如下:

▶總結

最後來總結這兩者,到底用哪個更好呢?

  • 推薦是兩者均開啟。

  • 如果對資料不敏感,可以選單獨用 RDB。

  • 如果只是做純記憶體快取,可以都不用。

▶ 參考文獻

  1. 黃健巨集.Redis設計與實現:機械工業出版社,2014

  1. http://redis.cn/topics/persistence.html

▶ 加入我們

我們來自位元組跳動飛書商業應用研發部(Lark Business Applications),目前我們在北京、深圳、上海、武漢、杭州、成都、廣州、三亞都設立了辦公區域。我們關注的產品領域主要在企業經驗管理軟體上,包括飛書 OKR、飛書績效、飛書招聘、飛書人事等 HCM 領域系統,也包括飛書審批、OA、法務、財務、採購、差旅與報銷等系統。歡迎各位加入我們。

掃碼發現職位&投遞簡歷