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 子程序對記憶體快照進行全量備份,是一個重量級操作,頻繁執行成本高。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CNwkDOyEmM0QjM0EWYyczMzYzXwAzM0EDMxAzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
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 持久化流程:
關鍵執行步驟如下
- Redis 父程序首先判斷:目前是否在執行 save,或 bgsave/bgrewriteaof 的子程序,如果在執行則 bgsave 指令直接傳回。bgsave/bgrewriteaof 的子程序不能同時執行,主要是基于性能方面的考慮:兩個并發的子程序同時執行大量的磁盤寫操作,可能引起嚴重的性能問題。
- 父程序執行 fork 操作建立子程序,這個過程中父程序是阻塞的,Redis 不能執行來自用戶端的任何指令。父程序 fork 後,bgsave 指令傳回”Background saving started”資訊并不再阻塞父程序,并可以響應其他指令。
- 子程序程序對記憶體資料生成快照檔案。
- 父程序在此期間接收的新的寫操作,使用 COW 機制寫入。
- 子程序完成快照寫入,替換舊 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 控制,有三種同步政策,各個值的含義如下:
-
:指令寫入 aof_buf 後立即調用系統 write 操作和系統 fsync 操作同步到 AOF 檔案,fsync 完成後線程傳回。這種情況下,每次有寫指令都要同步到 AOF 檔案,硬碟 IO 成為性能瓶頸,Redis 隻能支援大約幾百TPS寫入,嚴重降低了 Redis 的性能;即便是使用固态硬碟(SSD),每秒大約也隻能處理幾萬個指令,而且會大大降低 SSD 的壽命。可靠性較高,資料基本不丢失。always
-
:指令寫入 aof_buf 後調用系統 write 操作,不對 AOF 檔案做 fsync 同步;同步由作業系統負責,通常同步周期為30秒。這種情況下,檔案同步的時間不可控,且緩沖區中堆積的資料會很多,資料安全性無法保證。no
-
:指令寫入 aof_buf 後調用系統 write 操作,write 完成後線程傳回;fsync 同步檔案操作由專門的線程每秒調用一次。everysec 是前述兩種政策的折中,是性能和資料安全性的平衡,是以是 Redis 的預設配置,也是我們推薦的配置。everysec
檔案重寫(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 重寫流程如下:
- bgrewriteaof 觸發重寫,判斷是否存在 bgsave 或者 bgrewriteaof 正在執行,存在則等待其執行結束再執行
- 主程序 fork 子程序,防止主程序阻塞無法提供服務,類似 RDB
- 子程序周遊 Redis 記憶體快照中資料寫入臨時 AOF 檔案,同時會将新的寫指令寫入 aof_buf 和 aof_rewrite_buf 兩個重寫緩沖區,前者是為了寫回舊的 AOF 檔案,後者是為了後續重新整理到臨時 AOF 檔案中,防止快照記憶體周遊時新的寫入操作丢失
- 子程序結束臨時 AOF 檔案寫入後,通知主程序
- 主程序會将上面 3 中的 aof_rewirte_buf 緩沖區中的資料寫入到子程序生成的臨時 AOF 檔案中
- 主程序使用臨時 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。
- 如果隻是做純記憶體緩存,可以都不用。