Redis(7)——持久化【一文了解】


一、持久化簡介

Redis 的數據 全部存儲內存 中,如果 突然宕機,數據就會全部丟失,因此必須有一套機制來保證 Redis 的數據不會因為故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。

持久化發生了什么 | 從內存到磁盤

我們來稍微考慮一下 Redis 作為一個 "內存數據庫" 要做的關於持久化的事情。通常來說,從客戶端發起請求開始,到服務器真實地寫入磁盤,需要發生如下幾件事情:

詳細版 的文字描述大概就是下面這樣:

  1. 客戶端向數據庫 發送寫命令 (數據在客戶端的內存中)
  2. 數據庫 接收 到客戶端的 寫請求 (數據在服務器的內存中)
  3. 數據庫 調用系統 API 將數據寫入磁盤 (數據在內核緩沖區中)
  4. 操作系統將 寫緩沖區 傳輸到 磁盤控控制器 (數據在磁盤緩存中)
  5. 操作系統的磁盤控制器將數據 寫入實際的物理媒介(數據在磁盤中)

注意: 上面的過程其實是 極度精簡 的,在實際的操作系統中,緩存緩沖區 會比這 多得多...

如何盡可能保證持久化的安全

如果我們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 並且沒有接觸到內核,那么在 上述步驟 3 成功返回之后,我們就認為成功了。即使進程崩潰,操作系統仍然會幫助我們把數據正確地寫入磁盤。

如果我們考慮 停電/ 火災更具災難性 的事情,那么只有在完成了第 5 步之后,才是安全的。

機房”火了“

所以我們可以總結得出數據安全最重要的階段是:步驟三、四、五,即:

  • 數據庫軟件調用寫操作將用戶空間的緩沖區轉移到內核緩沖區的頻率是多少?
  • 內核多久從緩沖區取數據刷新到磁盤控制器?
  • 磁盤控制器多久把數據寫入物理媒介一次?
  • 注意: 如果真的發生災難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丟失,所以只能 盡可能地保證 數據的安全,這對於所有數據庫來說都是一樣的。

我們從 第三步 開始。Linux 系統提供了清晰、易用的用於操作文件的 POSIX file API20 多年過去,仍然還有很多人對於這一套 API 的設計津津樂道,我想其中一個原因就是因為你光從 API 的命名就能夠很清晰地知道這一套 API 的用途:

int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);

所以,我們有很好的可用的 API 來完成 第三步,但是對於成功返回之前,我們對系統調用花費的時間沒有太多的控制權。

然后我們來說說 第四步。我們知道,除了早期對電腦特別了解那幫人 (操作系統就這幫人搞的),實際的物理硬件都不是我們能夠 直接操作 的,都是通過 操作系統調用 來達到目的的。為了防止過慢的 I/O 操作拖慢整個系統的運行,操作系統層面做了很多的努力,譬如說 上述第四步 提到的 寫緩沖區,並不是所有的寫操作都會被立即寫入磁盤,而是要先經過一個緩沖區,默認情況下,Linux 將在 30 秒 后實際提交寫入。

image

但是很明顯,30 秒 並不是 Redis 能夠承受的,這意味着,如果發生故障,那么最近 30 秒內寫入的所有數據都可能會丟失。幸好 PROSIX API 提供了另一個解決方案:fsync,該命令會 強制 內核將 緩沖區 寫入 磁盤,但這是一個非常消耗性能的操作,每次調用都會 阻塞等待 直到設備報告 IO 完成,所以一般在生產環境的服務器中,Redis 通常是每隔 1s 左右執行一次 fsync 操作。

到目前為止,我們了解到了如何控制 第三步第四步,但是對於 第五步,我們 完全無法控制。也許一些內核實現將試圖告訴驅動實際提交物理介質上的數據,或者控制器可能會為了提高速度而重新排序寫操作,不會盡快將數據真正寫到磁盤上,而是會等待幾個多毫秒。這完全是我們無法控制的。

二、Redis 中的兩種持久化方式

方式一:快照

image

Redis 快照 是最簡單的 Redis 持久性模式。當滿足特定條件時,它將生成數據集的時間點快照,例如,如果先前的快照是在2分鍾前創建的,並且現在已經至少有 100 次新寫入,則將創建一個新的快照。此條件可以由用戶配置 Redis 實例來控制,也可以在運行時修改而無需重新啟動服務器。快照作為包含整個數據集的單個 .rdb 文件生成。

但我們知道,Redis 是一個 單線程 的程序,這意味着,我們不僅僅要響應用戶的請求,還需要進行內存快照。而后者要求 Redis 必須進行 IO 操作,這會嚴重拖累服務器的性能。

還有一個重要的問題是,我們在 持久化的同時內存數據結構 還可能在 變化,比如一個大型的 hash 字典正在持久化,結果一個請求過來把它刪除了,可是這才剛持久化結束,咋辦?

image

使用系統多進程 COW(Copy On Write) 機制 | fork 函數

操作系統多進程 COW(Copy On Write) 機制 拯救了我們。Redis 在持久化時會調用 glibc 的函數 fork 產生一個子進程,簡單理解也就是基於當前進程 復制 了一個進程,主進程和子進程會共享內存里面的代碼塊和數據段:

這里多說一點,為什么 fork 成功調用后會有兩個返回值呢? 因為子進程在復制時復制了父進程的堆棧段,所以兩個進程都停留在了 fork 函數中 (都在同一個地方往下繼續"同時"執行),等待返回,所以 一次在父進程中返回子進程的 pid,另一次在子進程中返回零,系統資源不夠時返回負數(偽代碼如下)

pid = os.fork()
if pid > 0:
  handle_client_request()  # 父進程繼續處理客戶端請求
if pid == 0:
  handle_snapshot_write()  # 子進程處理快照寫磁盤
if pid < 0:  
  # fork error

所以 快照持久化 可以完全交給 子進程 來處理,父進程 則繼續 處理客戶端請求子進程 做數據持久化,它 不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,然后序列化寫到磁盤中。但是 父進程 不一樣,它必須持續服務客戶端請求,然后對 內存數據結構進行不間斷的修改

這個時候就會使用操作系統的 COW 機制來進行 數據段頁面 的分離。數據段是由很多操作系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面復
制一份分離出來,然后 對這個復制的頁面進行修改。這時 子進程 相應的頁面是 沒有變化的,還是進程產生時那一瞬間的數據。

子進程因為數據沒有變化,它能看到的內存里的數據在進程產生的一瞬間就凝固了,再也不會改變,這也是為什么 Redis 的持久化 叫「快照」的原因。接下來子進程就可以非常安心的遍歷數據了進行序列化寫磁盤了。

方式二:AOF

image

快照不是很持久。如果運行 Redis 的計算機停止運行,電源線出現故障或者您 kill -9 的實例意外發生,則寫入 Redis 的最新數據將丟失。盡管這對於某些應用程序可能不是什么大問題,但有些使用案例具有充分的耐用性,在這些情況下,快照並不是可行的選擇。

AOF(Append Only File - 僅追加文件) 它的工作方式非常簡單:每次執行 修改內存 中數據集的寫操作時,都會 記錄 該操作。假設 AOF 日志記錄了自 Redis 實例創建以來 所有的修改性指令序列,那么就可以通過對一個空的 Redis 實例 順序執行所有的指令,也就是 「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。

為了展示 AOF 在實際中的工作方式,我們來做一個簡單的實驗:

./redis-server --appendonly yes  # 設置一個新實例為 AOF 模式

然后我們執行一些寫操作:

redis 127.0.0.1:6379> set key1 Hello
OK
redis 127.0.0.1:6379> append key1 " World!"
(integer) 12
redis 127.0.0.1:6379> del key1
(integer) 1
redis 127.0.0.1:6379> del non_existing_key
(integer) 0

前三個操作實際上修改了數據集,第四個操作沒有修改,因為沒有指定名稱的鍵。這是 AOF 日志保存的文本:

$ cat appendonly.aof 
*2
$6
SELECT
$1
0
*3
$3
set
$4
key1
$5
Hello
*3
$6
append
$4
key1
$7
 World!
*2
$3
del
$4
key1

如您所見,最后的那一條 DEL 指令不見了,因為它沒有對數據集進行任何修改。

就是這么簡單。當 Redis 收到客戶端修改指令后,會先進行參數校驗、邏輯處理,如果沒問題,就 立即 將該指令文本 存儲 到 AOF 日志中,也就是說,先執行指令再將日志存盤。這一點不同於 MySQLLevelDBHBase 等存儲引擎,如果我們先存儲日志再做邏輯處理,這樣就可以保證即使宕機了,我們仍然可以通過之前保存的日志恢復到之前的數據狀態,但是 Redis 為什么沒有這么做呢?

Emmm... 沒找到特別滿意的答案,引用一條來自知乎上的回答吧:

AOF 重寫

image

Redis 在長期運行的過程中,AOF 的日志會越變越長。如果實例宕機重啟,重放整個 AOF 日志會非常耗時,導致長時間 Redis 無法對外提供服務。所以需要對 AOF 日志 "瘦身"

Redis 提供了 bgrewriteaof 指令用於對 AOF 日志進行瘦身。其 原理 就是 開辟一個子進程 對內存進行 遍歷 轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日志文件 中。序列化完畢后再將操作期間發生的 增量 AOF 日志 追加到這個新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。

fsync

image

AOF 日志是以文件的形式存在的,當程序對 AOF 日志文件進行寫操作時,實際上是將內容寫到了內核為文件描述符分配的一個內存緩存中,然后內核會異步將臟數據刷回到磁盤的。

就像我們 上方第四步 描述的那樣,我們需要借助 glibc 提供的 fsync(int fd) 函數來講指定的文件內容 強制從內核緩存刷到磁盤。但 "強制開車" 仍然是一個很消耗資源的一個過程,需要 "節制"!通常來說,生產環境的服務器,Redis 每隔 1s 左右執行一次 fsync 操作就可以了。

Redis 同樣也提供了另外兩種策略,一個是 永不 fsync,來讓操作系統來決定合適同步磁盤,很不安全,另一個是 來一個指令就 fsync 一次,非常慢。但是在生產環境基本不會使用,了解一下即可。

Redis 4.0 混合持久化

image

重啟 Redis 時,我們很少使用 rdb 來恢復內存狀態,因為會丟失大量數據。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。

Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日志,通常這部分 AOF 日志很小:

於是在 Redis 重啟的時候,可以先加載 rdb 的內容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。

相關閱讀

  1. Redis(1)——5種基本數據結構 - https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/
  2. Redis(2)——跳躍表 - https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/
  3. Redis(3)——分布式鎖深入探究 - https://www.wmyskxz.com/2020/03/01/redis-3/
  4. Reids(4)——神奇的HyperLoglog解決統計問題 - https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/
  5. Redis(5)——億級數據過濾和布隆過濾器 - https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/
  6. Redis(6)——GeoHash查找附近的人https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/

擴展閱讀

  1. Redis 數據備份與恢復 | 菜鳥教程 - https://www.runoob.com/redis/redis-backup.html
  2. Java Fork/Join 框架 - https://www.cnblogs.com/cjsblog/p/9078341.html

參考資料

  1. Redis persistence demystified | antirez weblog (作者博客) - http://oldblog.antirez.com/post/redis-persistence-demystified.html
  2. 操作系統 — fork()函數的使用與底層原理 - https://blog.csdn.net/Dawn_sf/article/details/78709839
  3. 磁盤和內存讀寫簡單原理 - https://blog.csdn.net/zhanghongzheng3213/article/details/54141202
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這里,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM