Redis 中如何保證數據的不丟失,Redis 中的持久化是如何進行的

Redis 中數據的持久化

前言

我們知道 Redis 是記憶體資料庫,所有操作都在記憶體上完成。記憶體的話,伺服器斷電,記憶體上面的數據就會丟失了。這個問題顯然是需要解決的。

Redis 中引入了持久化來避免數據的丟失,主要有兩種持久化的方式 RDB 持久化和 AOF 持久化。

AOF 持久化

什麼是 AOF 持久化

AOF(Append Only File):通過保存資料庫執行的命令來記錄資料庫的狀態。

redis

AOF日誌對資料庫命令的保存順序是,Redis 先執行命令,把數據寫入記憶體,然後才記錄日誌。

為什麼要後記錄日誌呢

1、後寫,能夠避免記錄到錯誤的命令。因為是先執行命令,後寫入日誌,只有命令執行成功了,命令才能被寫入到日誌中。

2、避免阻塞當前的寫操作,是在命令執行後才記錄日誌,所以不會阻塞當前的寫操作。

AOF 的潛在風險

  • 1、如果命令執行成功,寫入日誌的時候宕機了,命令沒有寫入到日誌中,這時候就有丟失數據的風險了,因為這時候沒有寫入日誌,服務斷電之後,這部分數據就丟失了。

這種場景在別的地方也很常見,比如基於 MQ 實現分散式事務,也會出現業務處理成功 + 事務消息發送失敗這種場景,RabbitMQ,RocketMQ,Kafka 事務性,消息丟失和消息重複發送的處理策略

  • 2、AOF 的日誌寫入也是在主執行緒進行的,如果磁碟的壓力很大,寫入速度變慢了,會影響後續的操作。

這兩種情況可通過調整 AOF 文件的寫入磁碟的時機來避免

AOF 文件的寫入和同步

AOF 文件持久化的功能分成三個步驟,文件追加(append),文件寫入,文件同步(sync)。

AOF 文件在寫入磁碟之前是先寫入到 aof_buf 緩衝區中,然後通過調用 flushAppendOnlyFile 將緩衝區中的內容保存到 AOF 文件中。

寫入的策略通過 appendfsync 來進行配置

  • Always:同步寫回 每次寫操作命令執行完後,同步將 AOF 日誌數據寫回硬碟;

  • Everysec:每秒寫回 每次寫操作命令執行完後,先將命令寫入到 AOF 文件的內核緩衝區,然後每隔一秒將緩衝區里的內容寫回到硬碟;

  • No:作業系統控制的寫回 Redis 不在控制命令的寫會時機,交由系統控制。每次寫操作命令執行完成之後,命令會被放入到 AOF 文件的內核緩衝區,之後什麼時候寫入到磁碟,交由系統控制。

AOF 文件重寫機制

因為每次執行的命令都會被寫入到 AOF 文件中,隨著系統的運行,越來越多的文件會被寫入到 AOF 文件中,這樣 AOF 文件勢必會變得很大,這種情況該如何去處理呢?

為了解決這種情況,Redis 中引入了重寫的機制

什麼是重寫呢?

因為 AOF 文件中記錄的是每個命令的操作記錄,舉個🌰,比如當一個鍵值對被多條寫命令反覆修改時,AOF文件會記錄相應的多條命令,那麼重寫機制,就是根據這個鍵值對當前的最新狀態,為它生成對應的寫入命令,保存成一行操作命令。這樣就精簡了 AOF 文件的大小。

192.168.56.118:6379> set name "xiaoming"
OK
192.168.56.118:6379> get name
"xiaoming"
192.168.56.118:6379> set name "xiaozhang"
OK
192.168.56.118:6379> set name "xiaoli"
OK

# 重寫後就是
192.168.56.118:6379> set name "xiaoli"

簡單來講就是多變一,就是把 AOF 中日誌根據當前鍵值的狀態,合併成一條操作命令。

重寫之後的文件會保存到新的 AOF 文件中,這時候舊的 AOF 文件和新的 AOF 文件中鍵值對的狀態是一樣的。然後新的 AOF 文件會替換掉舊的 AOF 文件,這樣 重寫操作一直在進行,AOF 文件就不至於變的過大。

重寫是後台進行的, AOF 的重寫會放到子進程中進行的,使用子進程的優點:

1、子進程處理 AOF 期間,不會影響 Redis 主執行緒對數據的處理;

2、子進程擁有所在執行緒的數據副本,使用進程能夠避免鎖的使用,保證數據的安全。

這裡來看下,AOF 的處理流程

AOF 重寫也有一個緩衝區,當服務節接收到新的命令的是,如果在正在進行 AOF 重寫,命令同樣也會被發送到 AOF 緩衝區

redis

子進程執行 AOF 重寫的過程,服務端進程主要處理以下內容

1、接收並處理客戶端發送的命令;

2、將執行後的命令寫入到 AOF 緩衝區;

3、將執行後的命令也寫入到 AOF 重寫緩衝區;

AOF 緩衝區和 AOF 重寫緩衝區中的內容會被定期的同步到 AOF 文件和 AOF 重寫文件中

當子進程完成重寫的時候,會給父進程發送一個訊號,這時候父進程主要主要進行下面的兩步操作:

1、將 AOF 重寫緩衝區中的內容全部寫入到 AOF 重寫文件中,這時候重寫 AOF 文件保存的數據狀態是和服務端資料庫的狀態一致的;

2、將 AOF 重寫文件替換舊的 AOF 文件;

通過 AOF 的重寫操作,新的 AOF 文件不斷的替換舊的 AOF 文件,這樣就能控制 AOF 文件的大小

AOF 的數據還原

AOF 文件包了重建資料庫索引鎖需要的全部命令,所以只需要讀入並重新執行一遍 AOF 文件中保存的命令,即可還原服務關閉之前資料庫的狀態。

RDB 持久化

什麼是 RDB 持久化

RDB(Redis database):實現方式是將存在 Redis 記憶體中的數據寫入到 RDB 文件中保存到磁碟上從而實現持久化的。

和 AOF 不同的是 RDB 保存的是數據而不是操作,在進行數據恢復的時候,直接把 RDB 的文件讀入到記憶體,即可完成數據恢復。

redis

RDB 如何做記憶體快照

Redis 中對於如何備份數據到 RDB 文件中,提供了兩種方式

  • 1、save: 在主執行緒中執行,不過這種會阻塞 Redis 服務進程;

  • 2、bgsave: 主執行緒會 fork 出一個子進程來負責處理 RDB 文件的創建,不會阻塞主執行緒的命令操作,這也是 Redis 中 RDB 文件生成的默認配置;

對於 save 和 bgsave 這兩種快照方式,服務端是禁止這兩種方式同時執行的,防止產生競爭條件。

Redis 中可以使用 save 選項,來配置服務端執行 BGSAVE 命令的間隔時間

#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

save 900 1 就是服務端在900秒,讀數據進行了至少1次修改,就會觸發一次 BGSAVE 命令

save 300 10 就是服務端在300秒,讀數據進行了至少10次修改,就會觸發一次 BGSAVE 命令

快照時發生數據修改

舉個栗子🌰:我們在t時刻開始對記憶體數據內進行快照,假定目前有 2GB 的數據需要同步,磁碟寫入的速度是 0.1GB/s 那麼,快照的時間就是 20s,那就是在 t+20s 完成快照。

如果在 t+6s 的時候修改一個還沒有寫入磁碟的記憶體數據 test 為 test-hello。那麼就會破壞快照的完整性了,因為 t 時刻備份的數據已經被修改了。當然是希望在備份期間數據不能被修改。

如果不能被修改,就意味這在快照期間不能對數據進行修改操作,就如上面的栗子,快照需要進行20s,期間不允許處理數據更新操作,這顯然也是不合理的。

這裡需要聊一下 bgsave 是可以避免阻塞,不過需要注意的是避免阻塞和正常讀寫操作是有區別的。避免阻塞主執行緒確實沒有阻塞可以處理讀操作,但是為了保護快照的完整性,是不能修改快照期間的數據的。

這裡就需要引入一種新的處理方案,寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。

bgsave 子進程是由主執行緒 fork 生成的,所以是可以共享主執行緒的記憶體的,bgsave子進程運行後會讀取主執行緒中的記憶體數據,並且寫入到 RDB 文件中。

寫複製技術就是,如果主執行緒在記憶體快照期間修改了一塊記憶體,那麼這塊記憶體會被複制一份,生成該數據的副本,然後 bgsave 子進程在把這段記憶體寫入到 RDB 文件中。這樣就可以在快照期間進行數據的修改了。

redis

多久做一次快照

對於快照,如果做的太頻繁,可能會出現前一次快照還沒有處理完成,後面的快照數據馬上就進來了,同時過於頻繁的快照也會增加磁碟的壓力。

如果間隔時間過久,伺服器在兩次快照期間宕機,丟失的數據大小會隨著快照間隔時間的增長而增加。

是否可以選擇增量式快照呢?選擇增量式快照,我們就需要記住每個鍵值對的狀態,如果鍵值對很多,同樣也會引入很多記憶體空間,這對於記憶體資源寶貴的Redis來說,有些得不償失。

相較於 AOF 來對比,RDB 是會在數據恢復時,速度更快。但是 RDB 的記憶體快照同步頻率不太好控制,過多過少都有問題。

Redis 4.0中提出了一個混合使用 AOF 日誌和記憶體快照的方法。簡單來說,記憶體快照以一定的頻率執行,在兩次快照之間,使用AOF日誌記錄這期間的所有命令操作。

通過混合使用AOF日誌和記憶體快照的方法,RDB 快照的頻率不需要過於頻繁,在兩次 RDB 快照期間,使用 AOF 日誌來記錄,這樣也不用考慮 AOF 的文件過大問題,在下一次 RDB 快照開始的時候就可以刪除 AOF 文件了。

redis

過期的鍵如何持久化

在生成 RDB 文件的過程中,如果一個鍵已經過期,那麼其不會被保存到 RDB 文件中。在載入 RDB 的時候,要分兩種情況:

  • 1、如果 Redis 以主伺服器的模式運行,那麼會對 RDB 中的鍵進行時間檢查,過期的鍵不會被恢復到 Redis 中。

  • 2、如果 Redis 以從伺服器的模式運行,那麼 RDB 中所有的鍵都會被載入,忽略時間檢查。在從伺服器與主伺服器進行數據同步的時候,從伺服器的數據會先被清空,所以載入過期鍵不會有問題。

對於 AOF 來說,如果一個鍵過期了,那麼不會立刻對 AOF 文件造成影響。因為 Redis 使用的是惰性刪除和定期刪除,只有這個鍵被刪除了,才會往 AOF 文件中追加一條 DEL 命令。在重寫 AOF 的過程中,程式會檢查資料庫中的鍵,已經過期的鍵不會被保存到 AOF 文件中。

在運行過程中,對於主從複製的 Redis,主伺服器和從伺服器對於過期鍵的處理也不相同:

  • 1、對於主伺服器,一個過期的鍵被刪除了後,會向從伺服器發送 DEL 命令,通知從伺服器刪除對應的鍵;

  • 2、從伺服器接收到讀取一個鍵的命令時,即使這個鍵已經過期,也不會刪除,而是照常處理這個命令;

  • 3、從伺服器接收到主伺服器的 DEL 命令後,才會刪除對應的過期鍵。

這樣保證了數據的一致性,一個鍵值對存在於主伺服器,也必然存在於從伺服器。

總結

AOF

優點:AOF 中有三種策略可以進行選擇,AOF 的默認策略為每秒鐘 fsync 一次,在這種配置下,Redis 仍然可以保持良好的性能,並且就算髮生故障停機,也最多只會丟失一秒鐘的數據。

缺點:AOF 文件體積一般情況下比 RDB 文件體積大,並且數據還原速度也慢於 RDB。

RDB

優點:可以快速恢複數據,相比於 AOF 的順序,逐一執行操作命令,效率更高;

缺點:因為是記憶體快照,頻率過快,過慢,都會有響應的問題。過快,浪費磁碟資源,會給磁碟造成壓力,過慢會存在較多數據丟失的問題。

Redis 4.0中提出了一個混合使用 AOF 日誌和記憶體快照的方法,如果想要保證數據不丟失,這是一個比較好的選擇;

如果允許分鐘級別的數據丟失,可以只使用RDB;

如果只用AOF,優先使用 everysec 的配置選項,因為它在可靠性和性能之間取了一個平衡。

參考

【Redis核心技術與實戰】//time.geekbang.org/column/intro/100056701
【Redis設計與實現】//book.douban.com/subject/25900156/
【過期鍵與持久化】//segmentfault.com/a/1190000017526315
【Redis 中如何保證數據不丟失,持久化是如何進行的】//boilingfrog.github.io/2022/01/07/redis中如何進行數據持久化/

Tags: