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