Redis RDB 持久化

Redis 在最初的版本就提供了 RDB 持久化,在 1.100 版之後才新增了 AOF 持久化的特性。這篇文章探討 RDB 持久化的實現。

# 什麼是 RDB 持久化

爲了保證高效讀寫,Redis 所有數據都保存到內存中。但一旦進程發生崩潰就會導致數據丟失,所以需要一個持久化策略來保障數據安全。所謂 RDB 持久化,就是 Redis 將內存中存儲的數據全量地保存在 rdb 文件中,在默認的配置下這個文件是存放在 Redis 當前目錄下的 dump.rdb 文件。

在啓動 Redis 服務的時候,會加載 dump.rdb,將數據存儲到內存。也可以手動或自動地啓動  RDB 持久化。那 RDB 持久化觸發的時機是什麼呢?

# RDB 持久化的觸發

Redis 提供有 save 命令,執行該命令時,Redis 會遍歷所有的 key 然後將這些數據寫入到文件系統,直到寫入完成即爲持久化完成。由於 Redis 是單線程的,執行 save 命令的過程中會阻塞所有其他操作。Redis 也提供有 bgsave 命令(background save),顧名思義也就是後臺存儲,執行 bgsave 的過程不影響 Redis 的正常運行。不管是 save 還是 bgsave,都是用戶主動操作,Redis 也提供有自動執行持久化的配置,配置方式形如:

save 900 1
save 300 10
save 60 10000
save m n

表示在距離上一次執行備份的 m 秒內,如果至少有 n 個 key 被修改,則執行 RDB 持久化。當配置多條規則的時候,滿足其中一條則可。Redis 在全局有一個 dirty 計數器,在執行 RDB 持久化前,每寫入一個 key,dirty 計數器就會累加,上面的 n 就是 dirty 計數器的值,執行完 RDB 持久化後 dirty 計算器歸零, dirty 可以理解爲未被持久化的熱數據。

# 後臺存儲

Redis 是怎樣實現後臺存儲的?多線程和子進程都可以獲得 Redis 的內存數據,我們來看看這兩種方式實現的可行性。先來看看多線程的方案,Redis 創建多線程直接就共享當前線程的內存空間,但如果在後臺執行 RDB 持久化的過程中,Redis 有了新 key-value 改動,那麼在執行持久化的線程訪問到的內存數據也會是開始持久化動作後的新的數據,這樣就無法保證持久化數據的時間節點,這樣如果要使用線程,可能需要引入鎖的操作,這可能是個複雜並難以實現的方案。

再來看看子進程的方案,使用 fork 去創建子進程,子進程擁有父進程內存的一份完整拷貝,父子進程獨享內存,父進程的內存更新不會影響子進程的內存。這就避免了上述多線程帶來的問題。那是否子進程的方案就能滿足需求呢?這裏帶來兩個新的疑問:

    * 使用子進程,內存佔用會成倍增加嗎?即設 Redis 服務佔用 1G,那麼 fork 之後總內存佔用是不是變成了 2G?

    * 子進程的內存空間相當於父進程 fork 時內存空間的完整拷貝,那這個拷貝內存的過程,是否會佔用很多系統資源?

事實上,子進程拷貝內存採取的是寫時複製 (Copy On Write) 策略,能避免以上兩個問題。

父子進程理論上獨佔了一片內存空間。這個獨佔的內存空間指的是由操作系統抽象的虛擬內存空間,而它們虛擬內存映射的其實是同一片物理內存空間。如果共享部分的內存空間沒有發生更改,那麼父子進程實際上讀取的是同一片物理內存空間,既不會發生拷貝,也不會增加內存的佔用。而當父進程對共享部分內存進行修改的時候,系統會以頁爲單位將這部分內存拷貝到新的物理內存空間,子進程會訪問新的這部分內存空間。由於讀的時間一般遠大於寫的時間,所以子進程讀取內存空間時候一般不會產生大量的物理內存空間拷貝。總的來說使用 fork 創建子進程既不成倍地增加內存佔用,也不會因爲拷貝大量內存而佔用很多資源。

# 不影響原. rdb 文件

持久化成功後,數據會被保存到 dump.rdb 文件。那麼在執行持久化的過程中,dump.rdb 會上文件鎖嗎?假如執行持久化的同時,rdb 文件也正在被使用,會怎樣?舉一個常見的需求,每天可能會對 rdb 文件進行備份,執行下面的命令拷貝一份新的文件:

cp dump.rdb dump.2021.x.x

在執行 cp 的過程,會影響到 Redis 持久化嗎?

實際上並不會產生影響。Redis 在執行持久化的時候,並不是直接寫入 dump.rdb 文件,而是會先將所有數據保存在 tmp.rdb 這一個臨時文件,然後再使用 rename 方法將 tmp.rdb 改名爲 dump.rdb。查閱 rename 的文檔:

可以發現他有兩個特性:

* Open file descriptors for oldpath are also unaffected.

* If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing. 

如果舊的文件已經打開了文件描述符,rename 不影響這次使用。

如果新文件路徑已存在,會原子地替換這個文件。

擁有這兩個特性,上述的問題就能得以解決。

Redis 在寫入 tmp.rdb 的時候除了使用 write(),還需要 fflush() 和 fsync(),具體原因可通過《系統調用:write() 返回成功,數據寫入就完成了嗎?》這篇文章進行了解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/F76vEzB4AuWDf1m9-fWBxA