天天看點

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

一、持久化簡介

Redis 的資料 全部存儲 在 記憶體 中,如果 突然當機,資料就會全部丢失,是以必須有一套機制來保證 Redis 的資料不會因為故障而丢失,這種機制就是 Redis 的 持久化機制,它會将記憶體中的資料庫狀态 儲存到磁盤 中。

持久化發生了什麼 | 從記憶體到磁盤

我們來稍微考慮一下 Redis 作為一個 "記憶體資料庫" 要做的關于持久化的事情。通常來說,從用戶端發起請求開始,到伺服器真實地寫入磁盤,需要發生如下幾件事情:

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

  1. 用戶端向資料庫 發送寫指令 (資料在用戶端的記憶體中)
  2. 資料庫 接收 到用戶端的 寫請求 (資料在伺服器的記憶體中)
  3. 資料庫 調用系統 API 将資料寫入磁盤 (資料在核心緩沖區中)
  4. 作業系統将 寫緩沖區 傳輸到 磁盤控控制器 (資料在磁盤緩存中)
  5. 作業系統的磁盤控制器将資料 寫入實際的實體媒介 中 (資料在磁盤中)

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

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

如果我們故障僅僅涉及到 軟體層面 (該程序被管理者終止或程式崩潰) 并且沒有接觸到核心,那麼在 上述步驟 3 成功傳回之後,我們就認為成功了。即使程序崩潰,作業系統仍然會幫助我們把資料正确地寫入磁盤。

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

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

  • 資料庫軟體調用寫操作将使用者空間的緩沖區轉移到核心緩沖區的頻率是多少?
  • 核心多久從緩沖區取資料重新整理到磁盤控制器?
  • 磁盤控制器多久把資料寫入實體媒介一次?
  • 注意: 如果真的發生災難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丢失,是以隻能 盡可能地保證 資料的安全,這對于所有資料庫來說都是一樣的。

我們從 第三步 開始。Linux 系統提供了清晰、易用的用于操作檔案的

POSIX file API

20

多年過去,仍然還有很多人對于這一套

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 設計最佳實踐的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html

是以,我們有很好的可用的

API

來完成 第三步,但是對于成功傳回之前,我們對系統調用花費的時間沒有太多的控制權。

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

但是很明顯,30 秒 并不是 Redis 能夠承受的,這意味着,如果發生故障,那麼最近 30 秒内寫入的所有資料都可能會丢失。幸好

PROSIX API

提供了另一個解決方案:

fsync

,該指令會 強制 核心将 緩沖區 寫入 磁盤,但這是一個非常消耗性能的操作,每次調用都會 阻塞等待 直到裝置報告 IO 完成,是以一般在生産環境的伺服器中,Redis 通常是每隔 1s 左右執行一次

fsync

操作。

到目前為止,我們了解到了如何控制

第三步

第四步

,但是對于 第五步,我們 完全無法控制。也許一些核心實作将試圖告訴驅動實際送出實體媒體上的資料,或者控制器可能會為了提高速度而重新排序寫操作,不會盡快将資料真正寫到磁盤上,而是會等待幾個多毫秒。這完全是我們無法控制的。

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

方式一:快照

Redis 快照 是最簡單的 Redis 持久性模式。當滿足特定條件時,它将生成資料集的時間點快照,例如,如果先前的快照是在2分鐘前建立的,并且現在已經至少有 100 次新寫入,則将建立一個新的快照。此條件可以由使用者配置 Redis 執行個體來控制,也可以在運作時修改而無需重新啟動伺服器。快照作為包含整個資料集的單個

.rdb

檔案生成。

但我們知道,Redis 是一個 單線程 的程式,這意味着,我們不僅僅要響應使用者的請求,還需要進行記憶體快照。而後者要求 Redis 必須進行 IO 操作,這會嚴重拖累伺服器的性能。

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

使用系統多程序 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

快照不是很持久。如果運作 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 日志中,也就是說,先執行指令再将日志存盤。這一點不同于

MySQL

LevelDB

HBase

等存儲引擎,如果我們先存儲日志再做邏輯處理,這樣就可以保證即使當機了,我們仍然可以通過之前儲存的日志恢複到之前的資料狀态,但是 Redis 為什麼沒有這麼做呢?

Emmm... 沒找到特别滿意的答案,引用一條來自知乎上的回答吧:
  • @緣于專注 - 我甚至覺得沒有什麼特别的原因。僅僅是因為,由于AOF檔案會比較大,為了避免寫入無效指令(錯誤指令),必須先做指令檢查?如何檢查,隻能先執行了。因為文法級别檢查并不能保證指令的有效性,比如删除一個不存在的key。而MySQL這種是因為它本身就維護了所有的表的資訊,是以可以文法檢查後過濾掉大部分無效指令直接記錄日志,然後再執行。
  • 更多讨論參見:為什麼Redis先執行指令,再記錄AOF日志,而不是像其它存儲引擎一樣反過來呢? - https://www.zhihu.com/question/342427472

AOF 重寫

Redis 在長期運作的過程中,AOF 的日志會越變越長。如果執行個體當機重新開機,重放整個 AOF 日志會非常耗時,導緻長時間 Redis 無法對外提供服務。是以需要對 AOF 日志 "瘦身"。

Redis 提供了

bgrewriteaof

指令用于對 AOF 日志進行瘦身。其 原理 就是 開辟一個子程序 對記憶體進行 周遊 轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日志檔案 中。序列化完畢後再将操作期間發生的 增量 AOF 日志 追加到這個新的 AOF 日志檔案中,追加完畢後就立即替代舊的 AOF 日志檔案了,瘦身工作就完成了。

fsync

AOF 日志是以檔案的形式存在的,當程式對 AOF 日志檔案進行寫操作時,實際上是将内容寫到了核心為檔案描述符配置設定的一個記憶體緩存中,然後核心會異步将髒資料刷回到磁盤的。

就像我們 上方第四步 描述的那樣,我們需要借助

glibc

提供的

fsync(int fd)

函數來講指定的檔案内容 強制從核心緩存刷到磁盤。但 "強制開車" 仍然是一個很消耗資源的一個過程,需要 "節制"!通常來說,生産環境的伺服器,Redis 每隔 1s 左右執行一次

fsync

操作就可以了。

Redis 同樣也提供了另外兩種政策,一個是 永不

fsync

,來讓作業系統來決定合适同步磁盤,很不安全,另一個是 來一個指令就

fsync

一次,非常慢。但是在生産環境基本不會使用,了解一下即可。

Redis 4.0 混合持久化

重新開機 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,與您共同成長!

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

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