面試必問的 Redis:RDB、AOF、混合持久化
前言
本來說 Redis 分3篇,但是上周寫持久化時(shí)發(fā)現(xiàn)持久化的內(nèi)容還越多的,于是持久化就單拆一篇了。
我估計(jì)后面的主從復(fù)制、哨兵、集群內(nèi)容也是不少,所以說實(shí)話,我也不知道之前說的3篇會拆成幾篇了。

持久化機(jī)制的內(nèi)容大綱其實(shí)很早就有了,但是實(shí)際寫的時(shí)候斷斷續(xù)續(xù)寫了有兩周。
主要細(xì)節(jié)還是挺多的,在翻源碼的過程中,會遇到一些疑惑點(diǎn),也發(fā)現(xiàn)一些自己以前不知道的知識點(diǎn),所以自己也要花點(diǎn)時(shí)間去搞清楚。
慢工出細(xì)活吧,本文還是有很多非常細(xì)節(jié)的內(nèi)容的,如果能掌握,讓大廠面試官眼前一亮還是問題不大的。

正文
Redis 核心主流程
AOF 和 RDB 的持久化過程中,有不少操作是在時(shí)間事件?serverCron 中被觸發(fā)的。所以,這邊有必要先了解下 Redis 中的事件核心流程。
Redis 的服務(wù)器進(jìn)程就是一個(gè)事件循環(huán),最重要的有兩個(gè)事件:文件事件和時(shí)間事件。Redis 在服務(wù)器初始化后,會無限循環(huán),處理產(chǎn)生的文件事件和時(shí)間事件。
文件事件常見的有:接受連接(accept)、讀取(read)、寫入(write)、關(guān)閉連接(close)等。
時(shí)間事件中常見的就是 serverCron,redis 核心流程中通常也只有這個(gè)時(shí)間事件。serverCron 默認(rèn)配置下每100ms會被觸發(fā)一次,在該時(shí)間事件中,會執(zhí)行很多操作:清理過期鍵、AOF 后臺重寫、RDB 的 save point 的檢查、將 aof_buf 內(nèi)容寫到磁盤上(flushAppendOnlyFile 函數(shù))等等。
Redis 的核心主流程如下圖:

相關(guān)源碼在 server.c、ae.c,核心方法是:main、aeProcessEvents
Redis?的持久化機(jī)制有哪幾種
RDB、AOF、混合持久化(redis4.0引入)
RDB的實(shí)現(xiàn)原理、優(yōu)缺點(diǎn)
描述:類似于快照。在某個(gè)時(shí)間點(diǎn),將 Redis 在內(nèi)存中的數(shù)據(jù)庫狀態(tài)(數(shù)據(jù)庫的鍵值對等信息)保存到磁盤里面。RDB 持久化功能生成的 RDB 文件是經(jīng)過壓縮的二進(jìn)制文件。
命令:有兩個(gè) Redis 命令可以用于生成 RDB 文件,一個(gè)是 SAVE,另一個(gè)是 BGSAVE。
開啟:使用 save point 配置,滿足 save point 條件后會觸發(fā) BGSAVE 來存儲一次快照,這邊的 save point 檢查就是在上文提到的 serverCron 中進(jìn)行。
save point 格式:save
save?900?1?#900秒內(nèi)有1個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件save?300?10?#300秒內(nèi)有10個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件save?60?10000?#60秒內(nèi)有10000個(gè)key發(fā)生了變化,則觸發(fā)保存RDB文件
關(guān)閉:1)注釋掉所有save point 配置可以關(guān)閉 RDB 持久化。2)在所有 save point 配置后增加:save "",該配置可以刪除所有之前配置的 save point。
save ""SAVE:生成 RDB 快照文件,但是會阻塞主進(jìn)程,服務(wù)器將無法處理客戶端發(fā)來的命令請求,所以通常不會直接使用該命令。
BGSAVE:fork 子進(jìn)程來生成 RDB 快照文件,阻塞只會發(fā)生在 fork 子進(jìn)程的時(shí)候,之后主進(jìn)程可以正常處理請求,詳細(xì)過程如下圖:

fork:在 Linux 系統(tǒng)中,調(diào)用 fork() 時(shí),會創(chuàng)建出一個(gè)新進(jìn)程,稱為子進(jìn)程,子進(jìn)程會拷貝父進(jìn)程的 page table。如果進(jìn)程占用的內(nèi)存越大,進(jìn)程的 page table 也會越大,那么 fork 也會占用更多的時(shí)間。如果 Redis 占用的內(nèi)存很大,那么在 fork 子進(jìn)程時(shí),則會出現(xiàn)明顯的停頓現(xiàn)象。
RDB 的優(yōu)點(diǎn):
1)RDB 文件是是經(jīng)過壓縮的二進(jìn)制文件,占用空間很小,它保存了 Redis 某個(gè)時(shí)間點(diǎn)的數(shù)據(jù)集,很適合用于做備份。?比如說,你可以在最近的 24 小時(shí)內(nèi),每小時(shí)備份一次 RDB 文件,并且在每個(gè)月的每一天,也備份一個(gè) RDB 文件。這樣的話,即使遇上問題,也可以隨時(shí)將數(shù)據(jù)集還原到不同的版本。
2)RDB 非常適用于災(zāi)難恢復(fù)(disaster recovery):它只有一個(gè)文件,并且內(nèi)容都非常緊湊,可以(在加密后)將它傳送到別的數(shù)據(jù)中心。
3)RDB 可以最大化 redis 的性能。父進(jìn)程在保存 RDB 文件時(shí)唯一要做的就是 fork 出一個(gè)子進(jìn)程,然后這個(gè)子進(jìn)程就會處理接下來的所有保存工作,父進(jìn)程無須執(zhí)行任何磁盤 I/O 操作。
4)RDB 在恢復(fù)大數(shù)據(jù)集時(shí)的速度比 AOF 的恢復(fù)速度要快。
RDB 的缺點(diǎn):
1)RDB 在服務(wù)器故障時(shí)容易造成數(shù)據(jù)的丟失。RDB 允許我們通過修改 save point 配置來控制持久化的頻率。但是,因?yàn)?RDB 文件需要保存整個(gè)數(shù)據(jù)集的狀態(tài), 所以它是一個(gè)比較重的操作,如果頻率太頻繁,可能會對?Redis 性能產(chǎn)生影響。所以通常可能設(shè)置至少5分鐘才保存一次快照,這時(shí)如果 Redis 出現(xiàn)宕機(jī)等情況,則意味著最多可能丟失5分鐘數(shù)據(jù)。
2)RDB 保存時(shí)使用 fork 子進(jìn)程進(jìn)行數(shù)據(jù)的持久化,如果數(shù)據(jù)比較大的話,fork 可能會非常耗時(shí),造成 Redis 停止處理服務(wù)N毫秒。如果數(shù)據(jù)集很大且 CPU 比較繁忙的時(shí)候,停止服務(wù)的時(shí)間甚至?xí)揭幻搿?/span>
3)Linux fork 子進(jìn)程采用的是 copy-on-write 的方式。在 Redis 執(zhí)行 RDB 持久化期間,如果 client 寫入數(shù)據(jù)很頻繁,那么將增加 Redis 占用的內(nèi)存,最壞情況下,內(nèi)存的占用將達(dá)到原先的2倍。剛 fork 時(shí),主進(jìn)程和子進(jìn)程共享內(nèi)存,但是隨著主進(jìn)程需要處理寫操作,主進(jìn)程需要將修改的頁面拷貝一份出來,然后進(jìn)行修改。極端情況下,如果所有的頁面都被修改,則此時(shí)的內(nèi)存占用是原先的2倍。
相關(guān)源碼在 rdb.c,核心方法是:rdbSaveBackground、rdbSave
AOF的實(shí)現(xiàn)原理、優(yōu)缺點(diǎn)
描述:保存 Redis 服務(wù)器所執(zhí)行的所有寫操作命令來記錄數(shù)據(jù)庫狀態(tài),并在服務(wù)器啟動時(shí),通過重新執(zhí)行這些命令來還原數(shù)據(jù)集。
開啟:AOF 持久化默認(rèn)是關(guān)閉的,可以通過配置:appendonly yes 開啟。
關(guān)閉:使用配置 appendonly no 可以關(guān)閉 AOF 持久化。
AOF 持久化功能的實(shí)現(xiàn)可以分為三個(gè)步驟:命令追加、文件寫入、文件同步。
命令追加:當(dāng) AOF 持久化功能打開時(shí),服務(wù)器在執(zhí)行完一個(gè)寫命令之后,會將被執(zhí)行的寫命令追加到服務(wù)器狀態(tài)的 aof 緩沖區(qū)(aof_buf)的末尾。
文件寫入與文件同步:可能有人不明白為什么將 aof_buf 的內(nèi)容寫到磁盤上需要兩步操作,這邊簡單解釋一下。
Linux 操作系統(tǒng)中為了提升性能,使用了頁緩存(page cache)。當(dāng)我們將 aof_buf 的內(nèi)容寫到磁盤上時(shí),此時(shí)數(shù)據(jù)并沒有真正的落盤,而是在 page cache 中,為了將 page cache 中的數(shù)據(jù)真正落盤,需要執(zhí)行 fsync / fdatasync 命令來強(qiáng)制刷盤。這邊的文件同步做的就是刷盤操作,或者叫文件刷盤可能更容易理解一些。
在文章開頭,我們提過?serverCron 時(shí)間事件中會觸發(fā)?flushAppendOnlyFile 函數(shù),該函數(shù)會根據(jù)服務(wù)器配置的 appendfsync 參數(shù)值,來決定是否將 aof_buf 緩沖區(qū)的內(nèi)容寫入和保存到 AOF 文件。
appendfsync?參數(shù)有三個(gè)選項(xiàng):
1)always:每處理一個(gè)命令都將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫入并同步到AOF 文件,即每個(gè)命令都刷盤。
2)everysec:將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫入到 AOF 文件,如果上次同步 AOF 文件的時(shí)間距離現(xiàn)在超過一秒鐘, 那么再次對 AOF 文件進(jìn)行同步, 并且這個(gè)同步操作是異步的,由一個(gè)后臺線程專門負(fù)責(zé)執(zhí)行,即每秒刷盤1次。
3)no:將 aof_buf 緩沖區(qū)中的所有內(nèi)容寫入到 AOF 文件, 但并不對 AOF 文件進(jìn)行同步, 何時(shí)同步由操作系統(tǒng)來決定。即不執(zhí)行刷盤,讓操作系統(tǒng)自己執(zhí)行刷盤。
AOF 的優(yōu)點(diǎn)
1)AOF 比 RDB可靠。你可以設(shè)置不同的 fsync 策略:no、everysec 和 always。默認(rèn)是 everysec,在這種配置下,redis 仍然可以保持良好的性能,并且就算發(fā)生故障停機(jī),也最多只會丟失一秒鐘的數(shù)據(jù)。
2)AOF文件是一個(gè)純追加的日志文件。即使日志因?yàn)槟承┰蚨宋磳懭胪暾拿睿ū热鐚懭霑r(shí)磁盤已滿,寫入中途停機(jī)等等),?我們也可以使用 redis-check-aof 工具也可以輕易地修復(fù)這種問題。
3)當(dāng) AOF文件太大時(shí),Redis 會自動在后臺進(jìn)行重寫:重寫后的新 AOF 文件包含了恢復(fù)當(dāng)前數(shù)據(jù)集所需的最小命令集合。整個(gè)重寫是絕對安全,因?yàn)橹貙懯窃谝粋€(gè)新的文件上進(jìn)行,同時(shí) Redis 會繼續(xù)往舊的文件追加數(shù)據(jù)。當(dāng)新文件重寫完畢,Redis 會把新舊文件進(jìn)行切換,然后開始把數(shù)據(jù)寫到新文件上。
4)AOF 文件有序地保存了對數(shù)據(jù)庫執(zhí)行的所有寫入操作以 Redis 協(xié)議的格式保存, 因此 AOF 文件的內(nèi)容非常容易被人讀懂, 對文件進(jìn)行分析(parse)也很輕松。如果你不小心執(zhí)行了 FLUSHALL 命令把所有數(shù)據(jù)刷掉了,但只要 AOF 文件沒有被重寫,那么只要停止服務(wù)器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重啟 Redis , 就可以將數(shù)據(jù)集恢復(fù)到 FLUSHALL 執(zhí)行之前的狀態(tài)。
AOF 的缺點(diǎn)
1)對于相同的數(shù)據(jù)集,AOF 文件的大小一般會比 RDB 文件大。
2)根據(jù)所使用的 fsync 策略,AOF 的速度可能會比 RDB 慢。通常 fsync 設(shè)置為每秒一次就能獲得比較高的性能,而關(guān)閉 fsync 可以讓 AOF 的速度和 RDB 一樣快。
3)AOF 在過去曾經(jīng)發(fā)生過這樣的 bug :因?yàn)閭€(gè)別命令的原因,導(dǎo)致 AOF 文件在重新載入時(shí),無法將數(shù)據(jù)集恢復(fù)成保存時(shí)的原樣。(舉個(gè)例子,阻塞命令 BRPOPLPUSH 就曾經(jīng)引起過這樣的 bug ) 。雖然這種 bug 在 AOF 文件中并不常見, 但是相較而言, RDB 幾乎是不可能出現(xiàn)這種 bug 的。
相關(guān)源碼在 aof.c,核心方法是:feedAppendOnlyFile、flushAppendOnlyFile
混合持久化的實(shí)現(xiàn)原理、優(yōu)缺點(diǎn)
描述:混合持久化并不是一種全新的持久化方式,而是對已有方式的優(yōu)化。混合持久化只發(fā)生于 AOF 重寫過程。使用了混合持久化,重寫后的新 AOF 文件前半段是 RDB 格式的全量數(shù)據(jù),后半段是 AOF 格式的增量數(shù)據(jù)。
整體格式為:[RDB file][AOF tail]
開啟:混合持久化的配置參數(shù)為 aof-use-rdb-preamble,配置為 yes 時(shí)開啟混合持久化,在 redis 4 剛引入時(shí),默認(rèn)是關(guān)閉混合持久化的,但是在 redis 5 中默認(rèn)已經(jīng)打開了。
關(guān)閉:使用?aof-use-rdb-preamble no 配置即可關(guān)閉混合持久化。
混合持久化本質(zhì)是通過 AOF 后臺重寫(bgrewriteaof?命令)完成的,不同的是當(dāng)開啟混合持久化時(shí),fork 出的子進(jìn)程先將當(dāng)前全量數(shù)據(jù)以 RDB 方式寫入新的 AOF 文件,然后再將 AOF?重寫緩沖區(qū)(aof_rewrite_buf_blocks)的增量命令以 AOF 方式寫入到文件,寫入完成后通知主進(jìn)程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。
優(yōu)點(diǎn):結(jié)合 RDB 和 AOF?的優(yōu)點(diǎn), 更快的重寫和恢復(fù)。
缺點(diǎn):AOF?文件里面的 RDB?部分不再是 AOF 格式,可讀性差。
相關(guān)源碼在 aof.c,核心方法是:rewriteAppendOnlyFile
為什么需要 AOF?重寫
AOF 持久化是通過保存被執(zhí)行的寫命令來記錄數(shù)據(jù)庫狀態(tài)的,隨著寫入命令的不斷增加,AOF 文件中的內(nèi)容會越來越多,文件的體積也會越來越大。
如果不加以控制,體積過大的 AOF 文件可能會對 Redis 服務(wù)器、甚至整個(gè)宿主機(jī)造成影響,并且 AOF 文件的體積越大,使用 AOF 文件來進(jìn)行數(shù)據(jù)還原所需的時(shí)間就越多。
舉個(gè)例子, 如果你對一個(gè)計(jì)數(shù)器調(diào)用了 100 次 INCR , 那么僅僅是為了保存這個(gè)計(jì)數(shù)器的當(dāng)前值, AOF 文件就需要使用 100 條記錄。
然而在實(shí)際上, 只使用一條 SET 命令已經(jīng)足以保存計(jì)數(shù)器的當(dāng)前值了, 其余 99 條記錄實(shí)際上都是多余的。
為了處理這種情況, Redis 引入了 AOF 重寫:可以在不打斷服務(wù)端處理請求的情況下, 對 AOF 文件進(jìn)行重建(rebuild)。
AOF?重寫
描述:Redis 生成新的 AOF 文件來代替舊 AOF 文件,這個(gè)新的 AOF 文件包含重建當(dāng)前數(shù)據(jù)集所需的最少命令。具體過程是遍歷所有數(shù)據(jù)庫的所有鍵,從數(shù)據(jù)庫讀取鍵現(xiàn)在的值,然后用一條命令去記錄鍵值對,代替之前記錄這個(gè)鍵值對的多條命令。
命令:有兩個(gè) Redis 命令可以用于觸發(fā) AOF 重寫,一個(gè)是 BGREWRITEAOF 、另一個(gè)是 ?REWRITEAOF 命令;
開啟:AOF 重寫由兩個(gè)參數(shù)共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同時(shí)滿足這兩個(gè)條件,則觸發(fā) AOF 后臺重寫 BGREWRITEAOF。
//?當(dāng)前AOF文件比上次重寫后的AOF文件大小的增長比例超過100auto-aof-rewrite-percentage 100//?當(dāng)前AOF文件的文件大小大于64MBauto-aof-rewrite-min-size 64mb
關(guān)閉:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自動AOF重寫功能。
auto-aof-rewrite-percentage 0REWRITEAOF:進(jìn)行 AOF 重寫,但是會阻塞主進(jìn)程,服務(wù)器將無法處理客戶端發(fā)來的命令請求,通常不會直接使用該命令。
BGREWRITEAOF:fork 子進(jìn)程來進(jìn)行 AOF 重寫,阻塞只會發(fā)生在 fork 子進(jìn)程的時(shí)候,之后主進(jìn)程可以正常處理請求。
REWRITEAOF 和 BGREWRITEAOF 的關(guān)系與 SAVE 和 BGSAVE 的關(guān)系類似。
相關(guān)源碼在 aof.c,核心方法是:rewriteAppendOnlyFile
AOF 后臺重寫存在的問題
AOF 后臺重寫使用子進(jìn)程進(jìn)行從寫,解決了主進(jìn)程阻塞的問題,但是仍然存在另一個(gè)問題:子進(jìn)程在進(jìn)行 AOF 重寫期間,服務(wù)器主進(jìn)程還需要繼續(xù)處理命令請求,新的命令可能會對現(xiàn)有的數(shù)據(jù)庫狀態(tài)進(jìn)行修改,從而使得當(dāng)前的數(shù)據(jù)庫狀態(tài)和重寫后的 AOF 文件保存的數(shù)據(jù)庫狀態(tài)不一致。
如何解決 AOF 后臺重寫存在的數(shù)據(jù)不一致問題
為了解決上述問題,Redis 引入了 AOF 重寫緩沖區(qū)(aof_rewrite_buf_blocks),這個(gè)緩沖區(qū)在服務(wù)器創(chuàng)建子進(jìn)程之后開始使用,當(dāng) Redis 服務(wù)器執(zhí)行完一個(gè)寫命令之后,它會同時(shí)將這個(gè)寫命令追加到 AOF 緩沖區(qū)和 AOF 重寫緩沖區(qū)。
這樣一來可以保證:
1、現(xiàn)有 AOF 文件的處理工作會如常進(jìn)行。這樣即使在重寫的中途發(fā)生停機(jī),現(xiàn)有的 AOF 文件也還是安全的。
2、從創(chuàng)建子進(jìn)程開始,也就是 AOF 重寫開始,服務(wù)器執(zhí)行的所有寫命令會被記錄到 AOF 重寫緩沖區(qū)里面。
這樣,當(dāng)子進(jìn)程完成 AOF 重寫工作后,父進(jìn)程會在 serverCron 中檢測到子進(jìn)程已經(jīng)重寫結(jié)束,則會執(zhí)行以下工作:
1、將 AOF 重寫緩沖區(qū)中的所有內(nèi)容寫入到新 AOF 文件中,這時(shí)新 AOF 文件所保存的數(shù)據(jù)庫狀態(tài)將和服務(wù)器當(dāng)前的數(shù)據(jù)庫狀態(tài)一致。
2、對新的 AOF 文件進(jìn)行改名,原子的覆蓋現(xiàn)有的 AOF 文件,完成新舊兩個(gè) AOF 文件的替換。
之后,父進(jìn)程就可以繼續(xù)像往常一樣接受命令請求了。
相關(guān)源碼在 aof.c,核心方法是:rewriteAppendOnlyFileBackground
AOF 重寫緩沖區(qū)內(nèi)容過多怎么辦
將 AOF 重寫緩沖區(qū)的內(nèi)容追加到新 AOF 文件的工作是由主進(jìn)程完成的,所以這一過程會導(dǎo)致主進(jìn)程無法處理請求,如果內(nèi)容過多,可能會使得阻塞時(shí)間過長,顯然是無法接受的。
Redis 中已經(jīng)針對這種情況進(jìn)行了優(yōu)化:
1、在進(jìn)行 AOF 后臺重寫時(shí),Redis 會創(chuàng)建一組用于父子進(jìn)程間通信的管道,同時(shí)會新增一個(gè)文件事件,該文件事件會將寫入 AOF 重寫緩沖區(qū)的內(nèi)容通過該管道發(fā)送到子進(jìn)程。
2、在重寫結(jié)束后,子進(jìn)程會通過該管道盡量從父進(jìn)程讀取更多的數(shù)據(jù),每次等待可讀取事件1ms,如果一直能讀取到數(shù)據(jù),則這個(gè)過程最多執(zhí)行1000次,也就是1秒。如果連續(xù)20次沒有讀取到數(shù)據(jù),則結(jié)束這個(gè)過程。
通過這些優(yōu)化,Redis 盡量讓 AOF 重寫緩沖區(qū)的內(nèi)容更少,以減少主進(jìn)程阻塞的時(shí)間。
到此,AOF 后臺重寫的核心內(nèi)容基本告一段落,通過一張圖來看下其完整流程。

相關(guān)源碼在 aof.c,核心方法是:aofCreatePipes、aofChildWriteDiffData、rewriteAppendOnlyFile
RDB、AOF、混合持久,我應(yīng)該用哪一個(gè)?
一般來說, 如果想盡量保證數(shù)據(jù)安全性, 你應(yīng)該同時(shí)使用 RDB 和 AOF 持久化功能,同時(shí)可以開啟混合持久化。
如果你非常關(guān)心你的數(shù)據(jù), 但仍然可以承受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失, 那么你可以只使用 RDB 持久化。
如果你的數(shù)據(jù)是可以丟失的,則可以關(guān)閉持久化功能,在這種情況下,Redis 的性能是最高的。
使用 Redis 通常都是為了提升性能,而如果為了不丟失數(shù)據(jù)而將 appendfsync ?設(shè)置為 always 級別時(shí),對 Redis 的性能影響是很大的,在這種不能接受數(shù)據(jù)丟失的場景,其實(shí)可以考慮直接選擇 MySQL 等類似的數(shù)據(jù)庫。
服務(wù)啟動時(shí)如何加載持久化數(shù)據(jù)
簡單來說,如果同時(shí)啟用了 AOF 和 RDB,Redis 重新啟動時(shí),會使用 AOF 文件來重建數(shù)據(jù)集,因?yàn)橥ǔ碚f, AOF 的數(shù)據(jù)會更完整。
而在引入了混合持久化之后,使用 AOF 重建數(shù)據(jù)集時(shí),會通過文件開頭是否為“REDIS”來判斷是否為混合持久化。
完整流程如下圖所示:

相關(guān)源碼在 server.c,核心方法是:loadDataFromDisk
最后
當(dāng)你的才華還撐不起你的野心的時(shí)候,你就應(yīng)該靜下心來學(xué)習(xí),愿你在我這里能有所收獲。
如果你覺得本文寫的還不錯(cuò),對你有幫助,請通過【點(diǎn)贊】讓我知道,支持我寫出更好的文章。
推薦閱讀
面試必問的 Redis:數(shù)據(jù)結(jié)構(gòu)和基礎(chǔ)概念
兩年Java開發(fā)工作經(jīng)驗(yàn)面試總結(jié)
4 年 Java 經(jīng)驗(yàn)面試總結(jié)、心得體會
