Redis-AOF

Redis最普遍的使用場(chǎng)景便是當(dāng)作緩存使用,因?yàn)樗押蠖藬?shù)據(jù)庫(kù)中的數(shù)據(jù)存儲(chǔ)在內(nèi)存中,然后直接從內(nèi)存中讀取數(shù)據(jù),響應(yīng)速度會(huì)非常快。但是,這里也有一個(gè)絕對(duì)不能忽略的問題:一旦服務(wù)器宕機(jī),內(nèi)存中的數(shù)據(jù)將全部丟失。
想到的一個(gè)解決方案是,從后端數(shù)據(jù)庫(kù)恢復(fù)這些數(shù)據(jù),但這種方式存在兩個(gè)問題:一是,需要頻繁訪問數(shù)據(jù)庫(kù),會(huì)給數(shù)據(jù)庫(kù)帶來巨大的壓力;二是,這些數(shù)據(jù)是從慢速數(shù)據(jù)庫(kù)中讀取出來的,性能肯定比不上從 Redis 中讀取,導(dǎo)致使用這些數(shù)據(jù)的應(yīng)用程序響應(yīng)變慢。所以,對(duì) Redis 來說,實(shí)現(xiàn)數(shù)據(jù)的持久化,避免從后端數(shù)據(jù)庫(kù)中進(jìn)行恢復(fù),是至關(guān)重要的。

目前,Redis 的持久化主要有兩大機(jī)制,即 AOF(Append Only File)日志和 RDB 快照。下面,我通過這篇文章來介紹下AOF是什么。

AOF日志是如何實(shí)現(xiàn)的?

說到日志,我們比較熟悉的是數(shù)據(jù)庫(kù)的寫前日志(Write Ahead Log, WAL),也就是說,在實(shí)際寫數(shù)據(jù)前,先把修改的數(shù)據(jù)記到日志文件中,以便故障時(shí)進(jìn)行恢復(fù)。不過,AOF 日志正好相反,它是寫后日志,“寫后”的意思是 Redis 是先執(zhí)行命令,把數(shù)據(jù)寫入內(nèi)存,然后才記錄日志,如下圖所示:



傳統(tǒng)數(shù)據(jù)庫(kù)的日志,例如 redo log(重做日志),記錄的是修改后的數(shù)據(jù),而 AOF 里記錄的是 Redis 收到的每一條命令,這些命令是以文本形式保存的。

我們以 Redis 收到“set testkey testvalue”命令后記錄的日志為例,看看 AOF 日志的內(nèi)容。其中,“*3”表示當(dāng)前命令有三個(gè)部分,每部分都是由“+數(shù)字”開頭,后面緊跟著具體的命令、鍵或值。這里,“數(shù)字”表示這部分中的命令、鍵或值一共有多少字節(jié)。例如,“3 set”表示這部分有 3 個(gè)字節(jié),也就是“set”命令。


但是,為了避免額外的檢查開銷,Redis 在向 AOF 里面記錄日志的時(shí)候,并不會(huì)先去對(duì)這些命令進(jìn)行語法檢查。所以,如果先記日志再執(zhí)行命令的話,日志中就有可能記錄了錯(cuò)誤的命令,Redis 在使用日志恢復(fù)數(shù)據(jù)時(shí),就可能會(huì)出錯(cuò)。

而寫后日志這種方式,就是先讓系統(tǒng)執(zhí)行命令,只有命令能執(zhí)行成功,才會(huì)被記錄到日志中,否則,系統(tǒng)就會(huì)直接向客戶端報(bào)錯(cuò)。所以,Redis 使用寫后日志這一方式的一大好處是,可以避免出現(xiàn)記錄錯(cuò)誤命令的情況。

除此之外,AOF 還有一個(gè)好處:它是在命令執(zhí)行后才記錄日志,所以不會(huì)阻塞當(dāng)前的寫操作。

不過,AOF 也有兩個(gè)潛在的風(fēng)險(xiǎn)。

首先,如果剛執(zhí)行完一個(gè)命令,還沒有來得及記日志就宕機(jī)了,那么這個(gè)命令和相應(yīng)的數(shù)據(jù)就有丟失的風(fēng)險(xiǎn)。如果此時(shí) Redis 是用作緩存,還可以從后端數(shù)據(jù)庫(kù)重新讀入數(shù)據(jù)進(jìn)行恢復(fù),但是,如果 Redis 是直接用作數(shù)據(jù)庫(kù)的話,此時(shí),因?yàn)槊顩]有記入日志,所以就無法用日志進(jìn)行恢復(fù)了。

其次,AOF 雖然避免了對(duì)當(dāng)前命令的阻塞,但可能會(huì)給下一個(gè)操作帶來阻塞風(fēng)險(xiǎn)。這是因?yàn)?,AOF 日志也是在主線程中執(zhí)行的,如果在把日志文件寫入磁盤時(shí),磁盤寫壓力大,就會(huì)導(dǎo)致寫盤很慢,進(jìn)而導(dǎo)致后續(xù)的操作也無法執(zhí)行了。

仔細(xì)分析的話,你就會(huì)發(fā)現(xiàn),這兩個(gè)風(fēng)險(xiǎn)都是和 AOF 寫回磁盤的時(shí)機(jī)相關(guān)的。這也就意味著,如果我們能夠控制一個(gè)寫命令執(zhí)行完后 AOF 日志寫回磁盤的時(shí)機(jī),這兩個(gè)風(fēng)險(xiǎn)就解除了。

3中寫回策略

其實(shí),對(duì)于這個(gè)問題,AOF 機(jī)制給我們提供了三個(gè)選擇,也就是 AOF 配置項(xiàng) appendfsync 的三個(gè)可選值。

  • Always,同步寫回:每個(gè)寫命令執(zhí)行完,立馬同步地將日志寫回磁盤;
  • Everysec,每秒寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),每隔一秒把緩沖區(qū)中的內(nèi)容寫入磁盤;
  • No,操作系統(tǒng)控制的寫回:每個(gè)寫命令執(zhí)行完,只是先把日志寫到 AOF 文件的內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回磁盤。

針對(duì)避免主線程阻塞和減少數(shù)據(jù)丟失問題,這三種寫回策略都無法做到兩全其美。我們來分析下其中的原因。

  • “同步寫回”可以做到基本不丟數(shù)據(jù),但是它在每一個(gè)寫命令后都有一個(gè)慢速的落盤操作,不可避免地會(huì)影響主線程性能;
  • 雖然“操作系統(tǒng)控制的寫回”在寫完緩沖區(qū)后,就可以繼續(xù)執(zhí)行后續(xù)的命令,但是落盤的時(shí)機(jī)已經(jīng)不在 Redis 手中了,只要 AOF 記錄沒有寫回磁盤,一旦宕機(jī)對(duì)應(yīng)的數(shù)據(jù)就丟失了;
  • “每秒寫回”采用一秒寫回一次的頻率,避免了“同步寫回”的性能開銷,雖然減少了對(duì)系統(tǒng)性能的影響,但是如果發(fā)生宕機(jī),上一秒內(nèi)未落盤的命令操作仍然會(huì)丟失。所以,這只能算是,在避免影響主線程性能和避免數(shù)據(jù)丟失兩者間取了個(gè)折中。



    總結(jié)一下就是:想要獲得高性能,就選擇 No 策略;如果想要得到高可靠性保證,就選擇 Always 策略;如果允許數(shù)據(jù)有一點(diǎn)丟失,又希望性能別受太大影響的話,那么就選擇 Everysec 策略。

但是,按照系統(tǒng)的性能需求選定了寫回策略,并不是“高枕無憂”了。畢竟,AOF 是以文件的形式在記錄接收到的所有寫命令。隨著接收的寫命令越來越多,AOF 文件會(huì)越來越大。這也就意味著,我們一定要小心 AOF 文件過大帶來的性能問題。

這里的“性能問題”,主要在于以下三個(gè)方面:一是,文件系統(tǒng)本身對(duì)文件大小有限制,無法保存過大的文件;二是,如果文件太大,之后再往里面追加命令記錄的話,效率也會(huì)變低;三是,如果發(fā)生宕機(jī),AOF 中記錄的命令要一個(gè)個(gè)被重新執(zhí)行,用于故障恢復(fù),如果日志文件太大,整個(gè)恢復(fù)過程就會(huì)非常緩慢,這就會(huì)影響到 Redis 的正常使用。

AOF 重寫機(jī)制

簡(jiǎn)單來說,AOF 重寫機(jī)制就是在重寫時(shí),Redis 根據(jù)數(shù)據(jù)庫(kù)的現(xiàn)狀創(chuàng)建一個(gè)新的 AOF 文件,也就是說,讀取數(shù)據(jù)庫(kù)中的所有鍵值對(duì),然后對(duì)每一個(gè)鍵值對(duì)用一條命令記錄它的寫入。比如說,當(dāng)讀取了鍵值對(duì)“testkey”: “testvalue”之后,重寫機(jī)制會(huì)記錄 set testkey testvalue 這條命令。這樣,當(dāng)需要恢復(fù)時(shí),可以重新執(zhí)行該命令,實(shí)現(xiàn)“testkey”: “testvalue”的寫入。

為什么重寫機(jī)制可以把日志文件變小呢? 實(shí)際上,重寫機(jī)制具有“多變一”功能。所謂的“多變一”,也就是說,舊日志文件中的多條命令,在重寫后的新日志中變成了一條命令。

我們知道,AOF 文件是以追加的方式,逐一記錄接收到的寫命令的。當(dāng)一個(gè)鍵值對(duì)被多條寫命令反復(fù)修改時(shí),AOF 文件會(huì)記錄相應(yīng)的多條命令。但是,在重寫的時(shí)候,是根據(jù)這個(gè)鍵值對(duì)當(dāng)前的最新狀態(tài),為它生成對(duì)應(yīng)的寫入命令。這樣一來,一個(gè)鍵值對(duì)在重寫日志中只用一條命令就行了,而且,在日志恢復(fù)時(shí),只用執(zhí)行這條命令,就可以直接完成這個(gè)鍵值對(duì)的寫入了。


AOF重寫減少日志大小

當(dāng)我們對(duì)一個(gè)列表先后做了 6 次修改操作后,列表的最后狀態(tài)是[“D”, “C”, “N”],此時(shí),只用 LPUSH u:list “N”, “C”, "D"這一條命令就能實(shí)現(xiàn)該數(shù)據(jù)的恢復(fù),這就節(jié)省了五條命令的空間。對(duì)于被修改過成百上千次的鍵值對(duì)來說,重寫能節(jié)省的空間當(dāng)然就更大了。

不過,雖然 AOF 重寫后,日志文件會(huì)縮小,但是,要把整個(gè)數(shù)據(jù)庫(kù)的最新數(shù)據(jù)的操作日志都寫回磁盤,仍然是一個(gè)非常耗時(shí)的過程。這時(shí),我們就要繼續(xù)關(guān)注另一個(gè)問題了:重寫會(huì)不會(huì)阻塞主線程?

AOF 重寫會(huì)阻塞嗎?

和 AOF 日志由主線程寫回不同,重寫過程是由后臺(tái)子進(jìn)程 bgrewriteaof 來完成的,這也是為了避免阻塞主線程,導(dǎo)致數(shù)據(jù)庫(kù)性能下降。

重寫的過程可以總結(jié)為“一個(gè)拷貝,兩處日志”。

“一個(gè)拷貝”就是指,每次執(zhí)行重寫時(shí),主線程 fork 出后臺(tái)的 bgrewriteaof 子進(jìn)程。此時(shí),fork 會(huì)把主線程的內(nèi)存拷貝一份給 bgrewriteaof 子進(jìn)程,這里面就包含了數(shù)據(jù)庫(kù)的最新數(shù)據(jù)。然后,bgrewriteaof 子進(jìn)程就可以在不影響主線程的情況下,逐一把拷貝的數(shù)據(jù)寫成操作,記入重寫日志。

“兩處日志”又是什么呢?因?yàn)橹骶€程未阻塞,仍然可以處理新來的操作。此時(shí),如果有寫操作,第一處日志就是指正在使用的 AOF 日志,Redis 會(huì)把這個(gè)操作寫到它的緩沖區(qū)。這樣一來,即使宕機(jī)了,這個(gè) AOF 日志的操作仍然是齊全的,可以用于恢復(fù)。

而第二處日志,就是指新的 AOF 重寫日志。這個(gè)操作也會(huì)被寫到重寫日志的緩沖區(qū)。這樣,重寫日志也不會(huì)丟失最新的操作。等到拷貝數(shù)據(jù)的所有操作記錄重寫完成后,重寫日志記錄的這些最新操作也會(huì)寫入新的 AOF 文


AOF非阻塞時(shí)的重寫過程

總結(jié)來說,每次 AOF 重寫時(shí),Redis 會(huì)先執(zhí)行一個(gè)內(nèi)存拷貝,用于重寫;然后,使用兩個(gè)日志保證在重寫過程中,新寫入的數(shù)據(jù)不會(huì)丟失。而且,因?yàn)?Redis 采用額外的線程進(jìn)行數(shù)據(jù)重寫,所以,這個(gè)過程并不會(huì)阻塞主線程。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容