前言
本來說 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 save 300 10 save 60 10000關閉:1)注釋掉所有save point 配置可以關閉 RDB 持久化。2)在所有 save point 配置後增加:save "",該配置可以刪除所有之前配置的 save point。
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。
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb關閉:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自動AOF重寫功能。
auto-aof-rewrite-percentage 0REWRITEAOF:進行 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
最後
當你的才華還撐不起你的野心的時候,你就應該靜下心來學習,願你在我這裡能有所收穫。
如果你覺得本文寫的還不錯,對你有幫助,請通過【點讚】讓我知道,支持我寫出更好的文章。
推薦閱讀
面試必問的 Redis:數據結構和基礎概念
兩年Java開發工作經驗面試總結
4 年 Java 經驗面試總結、心得體會
面試必問的 Spring,你懂了嗎?
如何寫一份讓 HR 眼前一亮的簡歷(附模板)
字節、美團、快手核心部門面試總結(真題解析)
面試阿里,HashMap 這一篇就夠了
面試必問的線程池,你懂了嗎?
BATJTMD 面試必問的 MySQL,你懂了嗎?
如何準備好一場大廠面試
跳槽,如何選擇一家公司
MySQL 8.0 MVCC 核心源碼解析