Redis持久化:RDB和AOF
Redis 數據存儲在內存中,如果不想辦法將數據保存到硬盤上,一旦Redis重啟(退出/故障),內存的數據將會全部丟失。我們肯定不想 Redis 里的數據由于某些故障全部丟失(導致所有請求都走 MySQL),即便發生了故障也希望可以將Redis原有的數據恢復過來,這就是持久化的作用。
Redis 提供了兩種不同的持久化方法來將數據存儲到硬盤里邊:
- **RDB(Redis Database)**,將某一時刻的所有數據保存到一個 RDB 文件中。
- **AOF(append-only-file)**,當Redis服務器執行寫命令的時候,將執行的寫命令保存到 AOF 文件中。
RDB內存快照,讓宕機快速恢復
1.什么是RDB內存快照?
在 Redis 執行“寫”指令的過程中,內存數據一直會變化,所謂內存快照,指的就是 Redis 內存中數據在某一時刻的狀態數據,好比時間定格在某一時刻。當我們拍照時,通過照片就能把某一時刻的瞬間畫面完全記錄下來。Redis 跟這個類似,就是把某一刻的數據以文件的形式拍下來,寫到磁盤上,這個快照文件叫做 RDB 文件,RDB 就是 Redis Database 的縮寫。
2.生成RDB的策略
Redis 并不會在每次執行“寫”指令的時候都觸發 RDB 寫磁盤,只需要在執行內存快照的時候寫磁盤,這樣既保證了唯快不破,還實現了持久化,宕機快速恢復。
我們知道 Redis 的單線程模型決定了我們要盡可能地避免會阻塞主線程的操作,所以就需要盡可能地避免 RDB 文件生成阻塞主線程。為此Redis提供了兩個指令用于生成 RDB 文件:
- SAVE:會阻塞 Redis 服務器進程,服務器不能接收任何請求,直到 RDB 文件創建完畢為止。
- BGSAVE:fork 出一個子進程,由子進程來負責創建 RDB 文件,服務器進程可以繼續接收請求。
除了手動調用 SAVE 或者 BGSAVE 命令生成 RDB 文件之外,我們可以使用配置的方式來定期執行:在默認的配置下,如果以下的條件被觸發,就會執行 BGSAVE 命令。
save 900 1 #在900秒(15分鐘)之后,至少有1個key發生變化,
save 300 10 #在300秒(5分鐘)之后,至少有10個key發生變化
save 60 10000 #在60秒(1分鐘)之后,至少有10000個key發生變化
3.RDB實現原理
在RDB執行期間為了保證快照的數據一致性,只能處理讀操作,不能修改正在執行快照的數據,這種場景,Redis 是允許的。那 Redis 是如何實現一邊處理寫請求,同時生成RDB文件的呢?
Redis 使用操作系統的多進程寫時復制技術 COW(Copy On Write)來實現快照的持久化。
Redis 在持久化是會調用 glibc 的函數(linux系統中最底層的api)fork產生一個子進程,快照持久化完全交給子進程來處理,父進程繼續處理客戶端請求。子進程剛剛產生時,它和父進程共享內存里面的代碼段和數據段,這時可以將父子進程想象成一個連體嬰兒,共享身體。這是Linux操作系統的機制,為了節約內存資源,所以盡可能讓它們共享起來,在進程分離的一瞬間,內存的增長幾乎沒有明顯的變化。
BGSAVE 子進程可以共享主線程的所有內存數據,讀取主線程的數據并寫入到 RDB 文件。當主線程執行寫指令修改數據的時候,這個數據就會復制一份副本,BGSAVE 子進程讀取這個副本數據寫到 RDB 文件。
在執行 SAVE 或 BGSAVE 命令創建一個新的 RDB 文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB 文件中。這既保證了快照的完整性,也允許主線程同時對數據進行修改,避免了對正常業務的影響。
4.RDB的優缺點
優點
- RDB 文件是一個很簡潔的單文件,采用 二進制 + 數據壓縮 的方式寫磁盤,文件體積小,數據恢復速度快。
- RDB 的性能很好,需要進行持久化時,主進程會 fork 一個子進程出來,然后把持久化的工作交給子進程,自己不會有相關的I/O操作。
缺點
- RDB 容易造成數據的丟失。假設每5分鐘保存一次快照,如果 Redis 因為某些原因不能正常工作,那么從上次產生快照到 Redis 出現問題這段時間的數據就會丟失了。
- RDB 使用 fork() 產生子進程進行數據的持久化,會阻塞主線程,如果數據比較大的話可能就會花費點時間,造成 Redis 停止服務幾毫秒。如果數據量很大且CPU性能不是很好的時候,停止服務的時間甚至會到1秒。
另外,過于頻繁的執行全量數據快照,有兩個嚴重的性能開銷:
- 頻繁生成 RDB 文件寫入磁盤,磁盤壓力過大。可能會出現上一個 RDB 還未完成,下一個又開始生成,陷入死循環。
- fork 出 BGSAVE 子進程這個動作本身會阻塞主線程,主線程的內存越大,阻塞時間越長。
AOF寫后日志,避免宕機數據丟失
1.什么是AOF寫后日志?
AOF(Append Only File)寫后日志,AOF 持久化就是將修改數據庫狀態的命令保存到 AOF 文件中,被寫入的命令都是以 Redis 的命令請求協議格式保存的,Redis 的命令請求協議是純文本格式。
假設 AOF 日志記錄了 Redis 實例創建以來所有的修改指令序列,那么就可以通過一個空的 Redis 實例順序執行所有的指令,也就是“重放”,來恢復Redis當前實例的內存數據結構的狀態。
寫后日志和寫前日志的對比
寫前日志(WAL,Write Ahead Log):在實際寫數據之前,將修改的數據寫到日志文件中,故障恢復得以保證。比如 MySQL Innodb 存儲引擎中的 redo log(重做日志)便是記錄修改的數據日志,在實際修改數據前先記錄修改日志再執行修改數據。
寫后日志:先執行“寫”指令請求,將數據寫入內存,再記錄日志。
日志格式
當 Redis 接收到 “set key value” 命令將數據寫入到內存之后,會按照如下格式寫入 AOF 文件:
- *3:表示當前指令分為三個部分,每部分都是 “$ + 數字” 開頭,緊跟后面是該部分具體的命令、鍵、值
- 數字:表示這部分的命令、鍵、值占用的字節大小。比如 “$3” 表示這部分包含三個字節,也就是 set 指令。
寫后日志的好處
寫后日志避免了額外的檢查開銷,不需要對執行的命令進行語法檢查。如果使用寫前日志的話,就需要先檢查語法是否有誤,否則日志記錄了錯誤的命令,在使用日志恢復的時候就會報錯。另外,寫后記錄日志,避免了阻塞當前“寫”指令的執行。
2.寫回策略
使用 AOF 也不是萬無一失的,假如 Redis 剛執行完指令,還沒記錄日志就宕機了,就有可能丟失這個命令的相關數據;還有, AOF 避免了當前命令的阻塞,但是可能會給下一個命令帶來阻塞的風險。AOF 日志是主線程執行的,將日志寫入磁盤過程中,如果磁盤壓力過大就會導致磁盤寫操作很慢,導致后續的“寫”指令阻塞。
發現了沒,這兩個問題與磁盤寫回有關,如果能合理控制“寫”指令執行完后 AOF 日志寫回磁盤的時機,問題就可以迎刃而解。
為了提高文件的寫入效率,當用戶調用 write 函數,將一些數據寫入到文件時候,操作系統通常會將寫入數據暫時保存在一個內存緩沖區里,等到緩沖區的空間被填滿或者超過了制定的限制之后,才真正將緩沖區中的數據寫入到磁盤里面。
這種做法雖然提高了效率,但也為寫入數據帶來了安全問題,因為如果計算機發生停機,那么保存在內存緩沖區里的寫入數據將會丟失。為此系統提供了 fsync 和 fdatasync 兩個同步函數,它們可以強制讓操作系統立即將緩沖區中的數據寫入到硬盤里,從而確保寫入數據的安全性。
與之相對應 Redis 提供了 AOF 配置項 appendfsync 寫回策略來控制 AOF 持久化功能的效率和安全性。
appendfsync always # 同步寫回,寫指令執行完畢立即將 aof_buf 緩沖區中的內容寫到 AOF 文件。
appendfsync everysec # 每秒寫回,寫指令執行完畢,把日志寫到 aof_buf 緩沖區,每隔一秒同步到磁盤,該策略為AOF的默認策略。
appendfsync no # 操作系統控制,寫指令執行完畢,把日志寫到 aof_buf 緩沖區,由操作系統決定何時寫回磁盤。
3.AOF重寫機制
由于 AOF 記錄的是一個個指令的內容,這就會導致保存的文件太大,另外,故障恢復的時候需要執行每一個指令,如果日志文件太大,整個恢復過程就會非常慢。為此,Reids 設計了 AOF 重寫機制,提供了 bgrewriteaof 命令用于對 AOF 文件進行瘦身。
其原理就是開辟一個子進程對內存進行遍歷轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日志文件中,序列化完畢后再將操作期間發生的增量 AOF 日志追加到這個新的 AOF 日志文件中,追加完畢后立即替換舊的 AOF 日志文件。瘦身工作就完成了。
重寫機制有“多變一”的功能,將舊日志中的多條指令,在重寫后就變成了一條指令。如下所示:三條 lpush 命令,經過 AOF 重寫后生成一條,對于多次修改的場景,縮減效果明顯。
重寫過程
和 AOF 日志由主線程寫回不同,重寫過程實際是由后臺子進程 bgrewriteof 完成的,這也是為了避免阻塞主線程,導致性能下降。
總的來說,一共出現兩個日志,一次內存數據拷貝,分別是舊的 AOF 日志和新的 AOF 重寫日志和Redis 數據拷貝。大致流程如下圖所示:
在上圖中,Redis 會將重寫過程中接收到的“寫”指令操作同時記錄到舊的 AOF 緩沖區和新的 AOF 重寫緩沖區,這樣重寫日志也保存了最新的操作,等到拷貝數據的所有操作記錄重寫完成后,重寫緩沖區記錄的最新操作也會寫到新的 AOF 文件中。
每次 AOF 重寫時,Redis 會先執行一次內存拷貝,用于遍歷數據生成重寫記錄。防止 AOF 重寫過程失敗,導致原 AOF 文件被污染,無法做恢復使用。
使用兩個日志可以保證在重寫過程中,新寫入的數據不會丟失,并且保持數據的一致性。
4.AOF 的優點和缺點
優點
- AOF比RDB可靠。可以靈活制定不同的fsync策略。
- AOF日志文件是一個純追加的文件。就算是遇到突然停電的情況,也不會出現日志的定位或者損壞問題。
- 當AOF文件過大時,Redis會自動在后臺進行重寫。
- AOF以命令格式存儲于文件中,在數據恢復時,AOF文件比RDB文件更容易讓開發人員看懂,并加以修改。
缺點
- 在相同的數據集下,AOF文件的大小一般會比RDB文件大。
- 在某些fsync策略下,AOF的速度會比RDB慢。通常fsync設置為每秒一次就能獲得比較高的性能,而在禁止fsync的情況下速度可以達到RDB的水平。
混合日志模型
重啟 Redis 時,我們很少使用 RDB 來恢復內存狀態,因為可能丟失大量數據。通常采用 AOF 日志重放,但是重放 AOF 日志性能相對 RDB 來說要慢很多,在Redis實例很大的情況下,啟動需要花費很長時間。
Redis 4.0 為了解決這個問題,提供了一個新的持久化選項--混合持久化,將 RDB 文件的內容和增量 AOF 日志文件存放到一起,這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結束的這段時間發生的增量 AOF 日志,通常這部分日志很小。
在 Redis 重啟的時候,先加載 RDB 的內容,然后再重放增量 AOF 日志,這樣的操作既保證了 Redis 重啟速度,又降低數據丟失風險。
總結
- Redis 提供 RDB 快照持久化方案,記錄某一時刻數據狀態
- Redis 通過寫時復制技術設計了BGSAVE,避免執行快照期間對讀寫指令的影響。
- Redis 提供了 AOF 寫后日志持久化方案,記錄每一條操作指令。
- Redis 通過 AOF 重寫方案,避免 AOF文件過大。
- Redis 提供了混合持久化的方案,RDB + AOF 實現持久化保證數據可靠性,同時支持故障后的數據快速恢復。
參考
Redis設計與實(https://weread.qq.com/web/reader/d35323e0597db0d35bd957bk73532580243735b90b45ac8)
Redis核心技術與實戰(https://time.geekbang.org/column/intro/329)