談?wù)凴edis的持久化—AOF日志與RDB快照
一、前言
對于Mysql,數(shù)據(jù)是持久化在磁盤上的。如果誤刪數(shù)據(jù),可以使用binlog進行恢復(fù);突然宕機時,其本身可以借助redo log進行崩潰恢復(fù)。
更多關(guān)于Mysql日志的內(nèi)容,可以參考我的另外一篇文章數(shù)據(jù)庫日志——binlog、redo log、undo log掃盲
而對于Redis,一般是把數(shù)據(jù)直接存儲在內(nèi)存中。如果不做任何持久化工作,在出現(xiàn)宕機后,內(nèi)存中的全部數(shù)據(jù)就會丟失。
顯然,業(yè)務(wù)方是不能容忍這樣的情況發(fā)生的。好在Redis提供了一系列的持久化機制,分別是AOF日志與RDB快照。
二、AOF
AOF全稱是Append Only File,Redis每次執(zhí)行完一個寫類型的語句后,會將該語句以某種格式使用追加的方式順序?qū)懭階OF日志中。
值得注意的是,AOF是默認不開啟的。
AOF日志的格式
以winows為例,進入到redis安裝目錄中的redis.windows.conf中,將appendonly的值修改為yes,即可開啟AOF
- # 默認關(guān)閉
- appendonly yes
- # AOF的默認文件名稱
- appendfilename "appendonly.aof"
當執(zhí)行以下命令后
- set java helloworld
在appendonly.aof文件中,可以看到以下內(nèi)容
- *3 代表當前命令有3個部分
- $3 第1部分命令的長度,3個字符
- set 第1部分命令
- $4 第2部分命令的長度,4個字符
- java 第2部分命令
- $10 第3部分命令的長度,10個字符
- helloworld 第3部分命令
當我們首次使用某個客戶端執(zhí)行命令時,客戶端會自動幫我們補充select 0(即選擇編號0的數(shù)據(jù)庫),這個命令也會被保存在AOF日志中。
寫AOF日志的流程
大致的流程如圖所示
在server中,主線程執(zhí)行完命令之后,會立即將命令寫入AOF緩沖中。之后會調(diào)用系統(tǒng)函數(shù)write(),將命令寫入內(nèi)核緩沖區(qū),并返回給客戶端成功的響應(yīng)。
內(nèi)核會在合適的時機將內(nèi)核緩沖區(qū)的中的數(shù)據(jù)寫入到磁盤中。
我們設(shè)想其中某個階段宕機時,會不會產(chǎn)生不一致的情況:
1、如果命令執(zhí)行成功,但寫入AOF緩存前崩潰重啟,客戶端會收到執(zhí)行失敗或超時的響應(yīng)。重啟之后AOF文件中沒有該條數(shù)據(jù),這個時候,數(shù)據(jù)是一致的。
2、如果命令執(zhí)行成功,寫入AOF緩存成功,但調(diào)用write時崩潰重啟。其實這種情況和第一條一樣,恢復(fù)后數(shù)據(jù)還是一致的。
3、如果命令執(zhí)行、寫入AOF緩存與內(nèi)核緩存都成功,客戶端會收到成功的響應(yīng)。如果這個時候機器宕機,內(nèi)核緩沖區(qū)中的數(shù)據(jù)將會丟失,也就是最后的AOF文件缺少該條命令,恢復(fù)后,就會產(chǎn)生數(shù)據(jù)不一致的情況。
第3種情況發(fā)生時,就會出現(xiàn)數(shù)據(jù)不一致的后果。怎么處理呢,很簡單啊,變異步為同步不就行了嗎。
調(diào)用write寫入內(nèi)核緩沖區(qū)后,再調(diào)用fsync強制讓內(nèi)核緩沖區(qū)中的數(shù)據(jù)刷到磁盤上,刷盤成功后,再返回給客戶端響應(yīng)。
這樣的解決方式看似可以,但是刷盤的操作非常耗時。在Redis執(zhí)行大量命令的時候,會一直進行不斷的刷盤,當磁盤壓力過大時,會阻塞下一個命令的執(zhí)行,大大降低性能。
看來得把握刷盤的時機,刷得慢了,機器崩潰恢復(fù)后就會丟失大量數(shù)據(jù)。刷得快了,就會嚴重降低性能。
不過,Redis本身也提供了3種寫回策略。
寫回策略
- always 同步寫回。每執(zhí)行一條命令,寫完AOF日志后,再返回。
- everysec 每秒寫回。執(zhí)行命令后,將數(shù)據(jù)寫入到內(nèi)核緩沖區(qū)就返回。只有會有一個線程,執(zhí)行每秒刷盤的定時任務(wù)。
- no 由內(nèi)核自行控制的寫回。每執(zhí)行一條命令,將數(shù)據(jù)寫入到內(nèi)核緩沖區(qū)就返回。內(nèi)核會在合適的時機刷盤。
這3種策略體現(xiàn)了不同的刷盤頻率,因此擁有不同級別的一致性與性能。
always策略最大程度上保證數(shù)據(jù)不丟失,但性能最差。
no策略性能最好,但在機器崩潰重啟后會丟失比較多的數(shù)據(jù)。
everysec是一種折中的策略,較always有不錯的性能。在極端的情況下,只會丟失1秒內(nèi)的數(shù)據(jù),是比較推薦的方式。
redis.windows.conf中有appendfsync配置項,用來配置寫回策略,默認的策略是everysec 。
隨著Redis不斷記錄AOF日志,AOF日志文件將變得越來越大,用作恢復(fù)的時間也將越長。因此需要一種方式減少文件的大小,這時候AOF重寫就派上用場了。
AOF重寫
在出現(xiàn)觸發(fā)重寫的條件時(例如AOF文件達到某個閾值),Redis掃描整個庫的所有數(shù)據(jù),將數(shù)據(jù)以命令的方式記錄在新的AOF日志中,待記錄完成后,使用新的AOF日志替換舊的即可。
舊日志中,可能存有對同一個key的多次操作命令,重寫的目的就是取最后一次有效的命令,刪除那些歷史命令,從而達到瘦身、壓縮的效果。
剛才提到,AOF重寫會掃描整個庫的數(shù)據(jù),因此注定就是一個非常耗時的操作,那么就不會在主線程中做,而是通過主線程fork出一個子進程進行重寫的。
重寫的流程圖如下:
1、當AOF日志文件的大小超過執(zhí)行的閾值后,就會觸發(fā)AOF重寫
2、主線程fork出一個子進程,fork的過程仍然是阻塞的。fork完之后,主線程依然可以接受命令并處理
3、子進程與主線程共享一個實例的所有數(shù)據(jù),子進程會對整個實例進行掃描,將其中的數(shù)據(jù)以命令的格式寫入到重寫日志中。
4、在子進程重寫的過程中,主線程可以接受命令,假設(shè)這個時候執(zhí)行了一條寫命令。
5、主線程會將數(shù)據(jù)存入到庫中,利用寫時復(fù)制技術(shù),子進程不會感知到數(shù)據(jù)有任何變化。
6、主線程將日志先寫入AOF緩沖區(qū),再寫入重寫緩沖區(qū)。
7、由特定寫回策略,將緩沖區(qū)中的數(shù)據(jù)寫入到舊的AOF日志中。
8、當子進程結(jié)束掃描,并且將所有命令寫入重寫日志后,再將重寫緩沖區(qū)中的數(shù)據(jù)追加到重寫日志中。
9、最后一步,主線程感知到子進程重寫日志完成,于是使用新的日志文件替換舊的文件。
也許有人會發(fā)出以下的疑問
為什么是fork出子進程,直接使用子線程不是也可以嗎?
如果是創(chuàng)建出來一個子線程,那么主線程在寫入,子線程在讀取,是需要通過加鎖的方式來保證線程安全的,加鎖就意味著降低性能。
而如果是fork出來子進程,主線程和子進程同樣需要共享數(shù)據(jù),當主線程寫入數(shù)據(jù)的時候,會利用寫時復(fù)制技術(shù),避免加鎖。
什么是寫時復(fù)制?
大家應(yīng)該都知道三角函數(shù)吧,嗯,這和寫時復(fù)制沒什么關(guān)系。
CopyOnWriteArrayList就利用到了寫時復(fù)制,讀不加鎖,寫則是復(fù)制一份數(shù)組出來,在新的數(shù)組上進行修改,最后替換引用。非常適合應(yīng)用于讀多寫少的場景,缺點是在替換引用前,線程讀到的是舊數(shù)據(jù)。
主線程在fork出一個子進程的時候,會將自己的頁表(虛擬地址與物理地址的映射表)復(fù)制一份出來給子進程,而不是直接復(fù)制內(nèi)存。否則在重寫的時候,Redis占用內(nèi)存會立即翻倍。
這樣的話,子進程就可以隨意訪問主線程中的數(shù)據(jù)。而當主線程修改一些實例數(shù)據(jù)時,就會復(fù)制一份物理內(nèi)存出來,并變動主線程的頁表,在新的內(nèi)存地址上存儲寫之后的數(shù)據(jù)。因為沒有變動子進程的頁表,因此主線程寫入的數(shù)據(jù)對子進程不可見。
重寫AOF緩沖區(qū)的作用是什么?
CopyOnWriteArrayList的缺點在于讀到的可能是舊數(shù)據(jù),子進程在掃描的時候,其實掃描到的也是舊數(shù)據(jù),因此需要在重寫結(jié)束后做補償。
子進程在重寫的過程中,掃描的數(shù)據(jù)是fork動作結(jié)束的那一刻的快照。而在重寫的過程中,主線程依然可以執(zhí)行命令,那么這些多出來的寫命令就可以放在一個獨立的重寫緩沖區(qū)中。在重寫完成后,再將重寫緩沖區(qū)中的內(nèi)容追加到重寫日志中,這就保證了數(shù)據(jù)的一致。
盡管存在AOF重寫機制,但重寫后的日志文件還是大,恢復(fù)速度較慢。
有沒有一種直接存儲數(shù)據(jù),而不是存儲命令(命令的大小顯然大于數(shù)據(jù)本身)的方式呢?RDB就閃亮登場了!
三、RDB
RDB的全稱是Redis Database Backup,即數(shù)據(jù)備份。
會將某一時刻內(nèi)的所有數(shù)據(jù)生成一個快照文件。該文件是一種經(jīng)過壓縮的二進制文件,默認名稱為dump.rdb,可通過修改dbfilename參數(shù)來改變RDB文件名。
快照文件僅保存數(shù)據(jù),不保存額外的操作命令,且經(jīng)過壓縮,因此在恢復(fù)速度上快于AOF。但RDB沒法做到實時的持久化,而AOF可以基本做到。
如何讓Redis生成RDB文件
通過save命令手動觸發(fā)
直接在主線程中執(zhí)行,會阻塞其他命令
通過bgsave命令手動觸發(fā)
主線程fork出來一個子進程,由子進程去執(zhí)行備份。
整個fork的過程,是會阻塞主線程的。由于不會復(fù)制物理內(nèi)存,因此fork是快速的。
fork結(jié)束后,主線程依然可以執(zhí)行其他的命令。
通過配置自動觸發(fā)
redis.windows.conf中有如下的幾個配置可用于觸發(fā)生成RDB文件
- # 900秒內(nèi)至少出現(xiàn)1條寫命令就觸發(fā)
- save 900 1
- # 300秒內(nèi)至少出現(xiàn)10條寫命令就觸發(fā)
- save 300 10
- # 60秒內(nèi)至少出現(xiàn)10000條寫命令就觸發(fā)
- save 60 10000
這種方式,也是通過fork出一個子進程來做的。
三種方式的觸發(fā)流程
客戶端使用bgsave命令時,主線程fork出來子進程,由子進程完成備份。
在子進程備份期間,主線程依然可以執(zhí)行命令。但該條數(shù)據(jù)并不會被子進程掃描到,和AOF重寫一樣,都利用到了寫時復(fù)制。
既然RDB文件占用小,恢復(fù)速度快,那可以大幅增加RDB生成的頻率嗎?
那顯然是不可以的,有可能上一輪RDB還未生成,下一輪又開始了。而且也存在性能問題,save全程都會阻塞主線程,bgsave的fork操作同樣也會阻塞主線程。
當然,RDB這種方式,如果在持久化的過程中發(fā)生宕機,會丟失在上次備份之后產(chǎn)生的所有數(shù)據(jù)。
四、AOF與RDB的特點總結(jié)
下面使用一張表格來直觀地展示兩者之間的優(yōu)缺點
另外值得注意的是,當同時開啟AOF與RDB時,Redis會優(yōu)先使用AOF日志來恢復(fù)數(shù)據(jù)。
RDB相比而言,會丟失較多的數(shù)據(jù)。AOF只有在實例數(shù)據(jù)比較大的時候,恢復(fù)速度才慢。
五、Redis4.0混合持久化模式
既然AOF與RDB獨有各自的優(yōu)勢,能否結(jié)合二者的特點呢?
在Redis4.0中,出現(xiàn)了一個新的模式——混合持久化。具體來講,就是全量RDB+增量AOF,將兩種類型的日志文件存放在一起。
RDB可以以較低的頻率執(zhí)行,兩次RDB之間的產(chǎn)生的增量數(shù)據(jù)記錄在AOF日志中,因此增量AOF日志的文件很小。
因此Redis在恢復(fù)時,先加載RDB數(shù)據(jù),再重放增量的AOF日志。不需要像之前重放全量AOF日志,因此恢復(fù)效率大大提升。