前言
本來說 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檔案
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檔案大小的增長比例超過100auto-aof-rewrite-percentage 100 // 當前AOF檔案的檔案大小大於64MBauto-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