本來說 redis 分3篇,但是上周寫持久化時發現持久化的內容還越多的,于是持久化就單拆一篇了。
我估計后面的主從復制、哨兵、集群內容也是不少,所以說實話,我也不知道之前說的3篇會拆成幾篇了
持久化機制的內容大綱其實很早就有了,但是實際寫的時候斷斷續續寫了有兩周。
主要細節還是挺多的,在翻源碼的過程中,會遇到一些疑惑點,也發現一些自己以前不知道的知識點,所以自己也要花點時間去搞清楚。
慢工出細活吧,本文還是有很多非常細節的內容的,如果能掌握,讓大廠面試官眼前一亮還是問題不大的。
正文
Redis 核心主流程
AOF 和 RDB 的持久化過程中,有不少操作是在時間事件 serverCron 中被觸發的。所以,這邊有必要先了解下 Redis 中的事件核心流程。
Redis 的服務器進程就是一個事件循環,最重要的有兩個事件:文件事件和時間事件。Redis 在服務器初始化后,會無限循環,處理產生的文件事件和時間事件。
文件事件常見的有:接受連接(accept)、讀取(read)、寫入(write)、關閉連接(close)等。
時間事件中常見的就是 serverCron,redis 核心流程中通常也只有這個時間事件。serverCron 默認配置下每100ms會被觸發一次,在該時間事件中,會執行很多操作:清理過期鍵、AOF 后臺重寫、RDB 的 save point 的檢查、將 aof_buf 內容寫到磁盤上(flushAppendOnlyFile 函數)等等。
Redis 的核心主流程如下圖:
相關源碼在 server.c、ae.c,核心方法是:main、aeProcessEvents
Redis 的持久化機制有哪幾種
RDB、AOF、混合持久化(redis4.0引入)
RDB的實現原理、優缺點
描述:類似于快照。在某個時間點,將 Redis 在內存中的數據庫狀態(數據庫的鍵值對等信息)保存到磁盤里面。RDB 持久化功能生成的 RDB 文件是經過壓縮的二進制文件。
命令:有兩個 Redis 命令可以用于生成 RDB 文件,一個是 SAVE,另一個是 BGSAVE。
開啟:使用 save point 配置,滿足 save point 條件后會觸發 BGSAVE 來存儲一次快照,這邊的 save point 檢查就是在上文提到的 serverCron 中進行。
save point 格式:save <seconds> <changes>,含義是 Redis 如果在 seconds 秒內數據發生了 changes 次改變,就保存快照文件。例如 Redis 默認就配置了以下3個:
save 900 1 #900秒內有1個key發生了變化,則觸發保存RDB文件
save 300 10 #300秒內有10個key發生了變化,則觸發保存RDB文件
save 60 10000 #60秒內有10000個key發生了變化,則觸發保存RDB文件
關閉:1)注釋掉所有save point 配置可以關閉 RDB 持久化。2)在所有 save point 配置后增加:save "",該配置可以刪除所有之前配置的 save point。
save ""
SAVE:生成 RDB 快照文件,但是會阻塞主進程,服務器將無法處理客戶端發來的命令請求,所以通常不會直接使用該命令。
BGSAVE:fork 子進程來生成 RDB 快照文件,阻塞只會發生在 fork 子進程的時候,之后主進程可以正常處理請求,詳細過程如下圖:
fork:在 linux 系統中,調用 fork() 時,會創建出一個新進程,稱為子進程,子進程會拷貝父進程的 page table。如果進程占用的內存越大,進程的 page table 也會越大,那么 fork 也會占用更多的時間。如果 Redis 占用的內存很大,那么在 fork 子進程時,則會出現明顯的停頓現象。
RDB 的優點:
1)RDB 文件是是經過壓縮的二進制文件,占用空間很小,它保存了 Redis 某個時間點的數據集,很適合用于做備份。 比如說,你可以在最近的 24 小時內,每小時備份一次 RDB 文件,并且在每個月的每一天,也備份一個 RDB 文件。這樣的話,即使遇上問題,也可以隨時將數據集還原到不同的版本。
2)RDB 非常適用于災難恢復(disaster recovery):它只有一個文件,并且內容都非常緊湊,可以(在加密后)將它傳送到別的數據中心。
3)RDB 可以最大化 redis 的性能。父進程在保存 RDB 文件時唯一要做的就是 fork 出一個子進程,然后這個子進程就會處理接下來的所有保存工作,父進程無須執行任何磁盤 I/O 操作。
4)RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。
RDB 的缺點:
1)RDB 在服務器故障時容易造成數據的丟失。RDB 允許我們通過修改 save point 配置來控制持久化的頻率。但是,因為 RDB 文件需要保存整個數據集的狀態, 所以它是一個比較重的操作,如果頻率太頻繁,可能會對 Redis 性能產生影響。所以通常可能設置至少5分鐘才保存一次快照,這時如果 Redis 出現宕機等情況,則意味著最多可能丟失5分鐘數據。
2)RDB 保存時使用 fork 子進程進行數據的持久化,如果數據比較大的話,fork 可能會非常耗時,造成 Redis 停止處理服務N毫秒。如果數據集很大且 CPU 比較繁忙的時候,停止服務的時間甚至會到一秒。
3)Linux fork 子進程采用的是 copy-on-write 的方式。在 Redis 執行 RDB 持久化期間,如果 client 寫入數據很頻繁,那么將增加 Redis 占用的內存,最壞情況下,內存的占用將達到原先的2倍。剛 fork 時,主進程和子進程共享內存,但是隨著主進程需要處理寫操作,主進程需要將修改的頁面拷貝一份出來,然后進行修改。極端情況下,如果所有的頁面都被修改,則此時的內存占用是原先的2倍。
相關源碼在 rdb.c,核心方法是:rdbSaveBackground、rdbSave
AOF的實現原理、優缺點
描述:保存 Redis 服務器所執行的所有寫操作命令來記錄數據庫狀態,并在服務器啟動時,通過重新執行這些命令來還原數據集。
開啟:AOF 持久化默認是關閉的,可以通過配置:appendonly yes 開啟。
關閉:使用配置 appendonly no 可以關閉 AOF 持久化。
AOF 持久化功能的實現可以分為三個步驟:命令追加、文件寫入、文件同步。
命令追加:當 AOF 持久化功能打開時,服務器在執行完一個寫命令之后,會將被執行的寫命令追加到服務器狀態的 aof 緩沖區(aof_buf)的末尾。
文件寫入與文件同步:可能有人不明白為什么將 aof_buf 的內容寫到磁盤上需要兩步操作,這邊簡單解釋一下。
Linux 操作系統中為了提升性能,使用了頁緩存(page cache)。當我們將 aof_buf 的內容寫到磁盤上時,此時數據并沒有真正的落盤,而是在 page cache 中,為了將 page cache 中的數據真正落盤,需要執行 fsync / fdatasync 命令來強制刷盤。這邊的文件同步做的就是刷盤操作,或者叫文件刷盤可能更容易理解一些。
在文章開頭,我們提過 serverCron 時間事件中會觸發 flushAppendOnlyFile 函數,該函數會根據服務器配置的 appendfsync 參數值,來決定是否將 aof_buf 緩沖區的內容寫入和保存到 AOF 文件。
appendfsync 參數有三個選項:
1)always:每處理一個命令都將 aof_buf 緩沖區中的所有內容寫入并同步到AOF 文件,即每個命令都刷盤。
2)everysec:將 aof_buf 緩沖區中的所有內容寫入到 AOF 文件,如果上次同步 AOF 文件的時間距離現在超過一秒鐘, 那么再次對 AOF 文件進行同步, 并且這個同步操作是異步的,由一個后臺線程專門負責執行,即每秒刷盤1次。
3)no:將 aof_buf 緩沖區中的所有內容寫入到 AOF 文件, 但并不對 AOF 文件進行同步, 何時同步由操作系統來決定。即不執行刷盤,讓操作系統自己執行刷盤。
AOF 的優點
1)AOF 比 RDB可靠。你可以設置不同的 fsync 策略:no、everysec 和 always。默認是 everysec,在這種配置下,redis 仍然可以保持良好的性能,并且就算發生故障停機,也最多只會丟失一秒鐘的數據。
2)AOF文件是一個純追加的日志文件。即使日志因為某些原因而包含了未寫入完整的命令(比如寫入時磁盤已滿,寫入中途停機等等), 我們也可以使用 redis-check-aof 工具也可以輕易地修復這種問題。
3)當 AOF文件太大時,Redis 會自動在后臺進行重寫:重寫后的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。整個重寫是絕對安全,因為重寫是在一個新的文件上進行,同時 Redis 會繼續往舊的文件追加數據。當新文件重寫完畢,Redis 會把新舊文件進行切換,然后開始把數據寫到新文件上。
4)AOF 文件有序地保存了對數據庫執行的所有寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕松。如果你不小心執行了 FLUSHALL 命令把所有數據刷掉了,但只要 AOF 文件沒有被重寫,那么只要停止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重啟 Redis , 就可以將數據集恢復到 FLUSHALL 執行之前的狀態。
AOF 的缺點
1)對于相同的數據集,AOF 文件的大小一般會比 RDB 文件大。
2)根據所使用的 fsync 策略,AOF 的速度可能會比 RDB 慢。通常 fsync 設置為每秒一次就能獲得比較高的性能,而關閉 fsync 可以讓 AOF 的速度和 RDB 一樣快。
3)AOF 在過去曾經發生過這樣的 bug :因為個別命令的原因,導致 AOF 文件在重新載入時,無法將數據集恢復成保存時的原樣。(舉個例子,阻塞命令 BRPOPLPUSH 就曾經引起過這樣的 bug ) 。雖然這種 bug 在 AOF 文件中并不常見, 但是相較而言, RDB 幾乎是不可能出現這種 bug 的。
相關源碼在 aof.c,核心方法是:feedAppendOnlyFile、flushAppendOnlyFile
混合持久化的實現原理、優缺點
描述:混合持久化并不是一種全新的持久化方式,而是對已有方式的優化。混合持久化只發生于 AOF 重寫過程。使用了混合持久化,重寫后的新 AOF 文件前半段是 RDB 格式的全量數據,后半段是 AOF 格式的增量數據。
整體格式為:[RDB file][AOF tail]
開啟:混合持久化的配置參數為 aof-use-rdb-preamble,配置為 yes 時開啟混合持久化,在 redis 4 剛引入時,默認是關閉混合持久化的,但是在 redis 5 中默認已經打開了。
關閉:使用 aof-use-rdb-preamble no 配置即可關閉混合持久化。
混合持久化本質是通過 AOF 后臺重寫(bgrewriteaof 命令)完成的,不同的是當開啟混合持久化時,fork 出的子進程先將當前全量數據以 RDB 方式寫入新的 AOF 文件,然后再將 AOF 重寫緩沖區(aof_rewrite_buf_blocks)的增量命令以 AOF 方式寫入到文件,寫入完成后通知主進程將新的含有 RDB 格式和 AOF 格式的 AOF 文件替換舊的的 AOF 文件。
優點:結合 RDB 和 AOF 的優點, 更快的重寫和恢復。
缺點:AOF 文件里面的 RDB 部分不再是 AOF 格式,可讀性差。
相關源碼在 aof.c,核心方法是:rewriteAppendOnlyFile
為什么需要 AOF 重寫
AOF 持久化是通過保存被執行的寫命令來記錄數據庫狀態的,隨著寫入命令的不斷增加,AOF 文件中的內容會越來越多,文件的體積也會越來越大。
如果不加以控制,體積過大的 AOF 文件可能會對 Redis 服務器、甚至整個宿主機造成影響,并且 AOF 文件的體積越大,使用 AOF 文件來進行數據還原所需的時間就越多。
舉個例子, 如果你對一個計數器調用了 100 次 INCR , 那么僅僅是為了保存這個計數器的當前值, AOF 文件就需要使用 100 條記錄。
然而在實際上, 只使用一條 SET 命令已經足以保存計數器的當前值了, 其余 99 條記錄實際上都是多余的。
為了處理這種情況, Redis 引入了 AOF 重寫:可以在不打斷服務端處理請求的情況下, 對 AOF 文件進行重建(rebuild)。
AOF 重寫
描述:Redis 生成新的 AOF 文件來代替舊 AOF 文件,這個新的 AOF 文件包含重建當前數據集所需的最少命令。具體過程是遍歷所有數據庫的所有鍵,從數據庫讀取鍵現在的值,然后用一條命令去記錄鍵值對,代替之前記錄這個鍵值對的多條命令。
命令:有兩個 Redis 命令可以用于觸發 AOF 重寫,一個是 BGREWRITEAOF 、另一個是 REWRITEAOF 命令;
開啟:AOF 重寫由兩個參數共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同時滿足這兩個條件,則觸發 AOF 后臺重寫 BGREWRITEAOF。
// 當前AOF文件比上次重寫后的AOF文件大小的增長比例超過100
auto-aof-rewrite-percentage 100
// 當前AOF文件的文件大小大于64MB
auto-aof-rewrite-min-size 64mb
關閉:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自動AOF重寫功能。
auto-aof-rewrite-percentage 0
REWRITEAOF:進行 AOF 重寫,但是會阻塞主進程,服務器將無法處理客戶端發來的命令請求,通常不會直接使用該命令。
BGREWRITEAOF:fork 子進程來進行 AOF 重寫,阻塞只會發生在 fork 子進程的時候,之后主進程可以正常處理請求。
REWRITEAOF 和 BGREWRITEAOF 的關系與 SAVE 和 BGSAVE 的關系類似。
相關源碼在 aof.c,核心方法是:rewriteAppendOnlyFile
AOF 后臺重寫存在的問題
AOF 后臺重寫使用子進程進行從寫,解決了主進程阻塞的問題,但是仍然存在另一個問題:子進程在進行 AOF 重寫期間,服務器主進程還需要繼續處理命令請求,新的命令可能會對現有的數據庫狀態進行修改,從而使得當前的數據庫狀態和重寫后的 AOF 文件保存的數據庫狀態不一致。
如何解決 AOF 后臺重寫存在的數據不一致問題
為了解決上述問題,Redis 引入了 AOF 重寫緩沖區(aof_rewrite_buf_blocks),這個緩沖區在服務器創建子進程之后開始使用,當 Redis 服務器執行完一個寫命令之后,它會同時將這個寫命令追加到 AOF 緩沖區和 AOF 重寫緩沖區。
這樣一來可以保證:
1、現有 AOF 文件的處理工作會如常進行。這樣即使在重寫的中途發生停機,現有的 AOF 文件也還是安全的。
2、從創建子進程開始,也就是 AOF 重寫開始,服務器執行的所有寫命令會被記錄到 AOF 重寫緩沖區里面。
這樣,當子進程完成 AOF 重寫工作后,父進程會在 serverCron 中檢測到子進程已經重寫結束,則會執行以下工作:
1、將 AOF 重寫緩沖區中的所有內容寫入到新 AOF 文件中,這時新 AOF 文件所保存的數據庫狀態將和服務器當前的數據庫狀態一致。
2、對新的 AOF 文件進行改名,原子的覆蓋現有的 AOF 文件,完成新舊兩個 AOF 文件的替換。
之后,父進程就可以繼續像往常一樣接受命令請求了。
相關源碼在 aof.c,核心方法是:rewriteAppendOnlyFileBackground
AOF 重寫緩沖區內容過多怎么辦
將 AOF 重寫緩沖區的內容追加到新 AOF 文件的工作是由主進程完成的,所以這一過程會導致主進程無法處理請求,如果內容過多,可能會使得阻塞時間過長,顯然是無法接受的。
Redis 中已經針對這種情況進行了優化:
1、在進行 AOF 后臺重寫時,Redis 會創建一組用于父子進程間通信的管道,同時會新增一個文件事件,該文件事件會將寫入 AOF 重寫緩沖區的內容通過該管道發送到子進程。
2、在重寫結束后,子進程會通過該管道盡量從父進程讀取更多的數據,每次等待可讀取事件1ms,如果一直能讀取到數據,則這個過程最多執行1000次,也就是1秒。如果連續20次沒有讀取到數據,則結束這個過程。
通過這些優化,Redis 盡量讓 AOF 重寫緩沖區的內容更少,以減少主進程阻塞的時間。
到此,AOF 后臺重寫的核心內容基本告一段落,通過一張圖來看下其完整流程。
相關源碼在 aof.c,核心方法是:aofCreatePipes、aofChildWriteDiffData、rewriteAppendOnlyFile
RDB、AOF、混合持久,我應該用哪一個?
一般來說, 如果想盡量保證數據安全性, 你應該同時使用 RDB 和 AOF 持久化功能,同時可以開啟混合持久化。
如果你非常關心你的數據, 但仍然可以承受數分鐘以內的數據丟失, 那么你可以只使用 RDB 持久化。
如果你的數據是可以丟失的,則可以關閉持久化功能,在這種情況下,Redis 的性能是最高的。
使用 Redis 通常都是為了提升性能,而如果為了不丟失數據而將 appendfsync 設置為 always 級別時,對 Redis 的性能影響是很大的,在這種不能接受數據丟失的場景,其實可以考慮直接選擇 MySQL 等類似的數據庫。
服務啟動時如何加載持久化數據
簡單來說,如果同時啟用了 AOF 和 RDB,Redis 重新啟動時,會使用 AOF 文件來重建數據集,因為通常來說, AOF 的數據會更完整。
而在引入了混合持久化之后,使用 AOF 重建數據集時,會通過文件開頭是否為“REDIS”來判斷是否為混合持久化。
完整流程如下圖所示:
相關源碼在 server.c,核心方法是:loadDataFromDisk