Redis 的持久化

壹言發表於2021-05-09

原文連結: https://www.changxuan.top/?p=1386


 

Redis 是一個非關係型的記憶體資料庫,使用記憶體儲存資料是它能夠進行快速存取資料的原因之一。

在實際應用中,常有人提倡把 Redis 只作為一種能夠提高使用者體驗的元件來使用, 也就是說即使 Redis 服務掛掉之後也要保證系統正常使用。不過,在很多系統中還是希望既能發揮 Redis 基於記憶體快速存取的特性,又希望機器斷電或 Redis服務停止後資料不丟失。所以,才引出了 Redis 的持久化功能。

在許多技術文章中,提到 Redis 的持久化時往往都會直接丟擲兩個名詞 RDB 和 AOF。然後接下來就是分別介紹這兩個名詞。當然,如果要談 Redis 的持久化肯定避免不了講 RDB 和 AOF,但這是介紹持久化最恰當的方式嗎?這樣的文章是不是顯得有些生硬呢?所以,在嘗試弄明白一個事物的原理時一定要從頭到尾的思考它存在的意義?為了解決什麼問題?採用了什麼方式?達到了什麼目的?自己有沒有其它的方案?這樣從問題的源頭切入,才能對這個事物理解的更加深刻,從而能夠更好的幫助自己進行舉一反三。而不是人云亦云,對於一些知識僅僅是背誦下來,這種死記硬背下來的知識在腦海裡的保質期也是短的可憐。

在前面,我們已經提到為什麼需要引入持久化?簡單的來說持久化就是把記憶體中的資料儲存到外存上,這樣服務停止後,當再啟動的時候就可以把外存的資料讀取到記憶體中從而達到了不丟失資料的目的。

RDB

如果讓你設計一個持久化的方案,你會怎麼做呢?(假裝絞盡腦汁… …)首先,我們可以使用一種簡單的策略,將 Redis 中所有的資料按照一定格式全部寫到磁碟上,即建立資料的快照檔案。然後,你為了儘量保證不丟資料需要考慮使用實時寫還是定時寫,又或者用其它策略。其實,現在的你已經在嘗試著去實現 RDB (Redis Database)持久化的機制了。所以,你看它其實並不難。萬丈高樓從地起,先從一個簡單的 idea 開始,逐漸去完善它,豐富它的過程便是解決問題的過程。例如用這種思路去學習計算機網路也是同樣適用的,你可以給自己出一個問題“如何讓兩臺電腦進行通訊?”,自己想辦法解決這個問題的過程肯定會比在計算機網路課堂上收穫的知識更多,也更牢固。

儘管不需要我們寫程式碼來實現 RDB 持久化,但是並不妨礙我們來思考一下假如讓我們來實現的話大概會遇到哪些問題?例如:什麼時候生成資料快照?檔案資料格式的定義?如果在主程式中進行持久化,阻塞客戶端的請求後會不會有影響?接下來,我們就看一下 RDB 是如何做的吧。

基本命令

在 Redis 中,提供了兩個 RDB 持久化的命令: SAVE 和 BGSAVE 。執行 SAVE 時,Redis 服務會停止處理任何客戶端的命令請求;執行 BGSAVE 時,Redis 服務則會建立一個子程式,由子程式來負責資料的持久化,而此時 Redis 服務就可以正常處理客戶端的請求。

BGSAVE 解決了我們對於持久化時是否會影響 Redis 服務處理客戶端的請求的擔心。

自動間隔性儲存

自動間隔性儲存,則解決了“什麼時候生成資料快照?”的問題。在 Redis 的配置檔案中我們可以寫入以下配置:

save 600 1
save 300 10
save 60 100
save 30 1000

上面的配置表示,如果在 600 秒內對資料庫進行了 1 次修改,就執行執行一次 BGSAVE 命令;如果在 300 秒內對資料庫進行了 10 次修改,就執行一次 BGSAVE 命令;以此類推。你可以根據你的業務場景,配置 save 的引數,也不僅僅侷限於 4 條配置。

實現原理

在 Redis 啟動時,會把上述配置儲存到 Redis 伺服器的狀態中,具體的結構體則是 redisServer,儲存 save 引數的結構體為 saveparam。

 1 // Redis 伺服器狀態資訊結構體
 2 struct redisServer {
 3     // ... ...
 4  
 5     // 記錄多個 save 配置引數
 6     struct saveparam *saveparams;
 7     // 修改次數計數器
 8     long long dirty;
 9     // 上次執行儲存的時間
10     time_t lastsave;
11  
12     // ... ...
13 }
14 // Save 引數結構體 saveparam
15 struct saveparam {
16     // 秒數
17     time_t seconds;
18     // 修改數
19     int changes;
20 }

 

看到上面 redisServer 結構體的屬性資訊,你心裡應該有答案了吧?dirty 表示的是自從上次執行 SAVE 或者 BGSAVE 命令完成之後對資料庫進行修改的次數;lastsave 表示的是上次成功執行SAVE 或者 BGSAVE 命令的時間。這個時候,如果再有個機制能夠定時檢查是否有滿足條件的配置引數就可以了。

Redis 提供了一個週期性操作函式 serverCron,每 100 ms 會執行一次。它其中的一項工作就是來檢查是否有符合條件的 save 引數,如果存在符合條件的引數則執行 BGSAVE 命令,執行完畢之後將 dirty 和 lastsave 的值重置。相信只要有基礎的程式設計知識,根據這些變數就能實現這個檢查的過程吧。

檔案結構

RDB 檔案結構示意圖

在上圖中,大寫字母的單詞表示的常量,小寫字母單詞則是變數和資料。RDB 檔案開頭的“REDIS”是我們習慣稱為的魔數,類似於 class 檔案的 COFFEE,用來識別檔案型別;緊接著長度為四個位元組的 db_version 記錄的是 RDB 檔案的版本號;database 表示的是所儲存的資料;EOF 則表明資料內容結束了;check_sum 的值是整個檔案的校驗和,用來檢查檔案是否損壞。

AOF

其實持久化資料除了 RDB 這種方式,肯定會有同學能想到另一種方式,就是把服務端執行的所有客戶端請求增加、修改和刪除等會改變資料的命令全都儲存起來。通過儲存這些命令資料,在遇到機器當機和服務程式異常中斷的情況下重啟服務時只要執行一遍這些持久化的命令即可恢復之前的資料了。(也是一個相當好的辦法呀!)

原理就是如此,那麼問題來了,假如同樣讓你來實現這個過程,你會考慮到哪些問題呢?

一是效能問題,執行完命令之後是否直接將此命令持久化到磁碟上還是由作業系統控制檔案同步?在這個問題上如何做取捨?二是檔案大小問題,隨著 Redis 服務執行越來越久,資料檔案勢必會越來越大?應該使用什麼辦法解決?… …

我們來看下 Redis 的 AOF 的過程吧!

持久化過程

首先,通過在配置檔案中增加一行配置 appendonly yes 來開啟 AOF 持久化。

像 RDB 機制所依賴 redisServer 結構體中的 saveparams、dirty、lastsave 引數一樣,AOF 的實現依賴 redisServer 結構體中的 aof_buf 引數。

1 struct redisServer{
2     // ... ...
3  
4     // AOF 緩衝區
5     sds aof_buf;
6  
7     // ... ...
8 }

 

aof_buf 引數用來以協議格式快取會對資料進行變更的命令。

在 Redis 伺服器執行完命令,並將命令以協議的格式追加到 aof_buf 緩衝區之後,在當前這個事件迴圈結束之前,Redis 還會呼叫一個函式 flushAppendOnlyFile,這個函式會根據配置檔案中 appendfsync 的值來決定接下來的持久化行為。appendfsync 有三個可選值,分別是 always、everysec、no

  • always: 將 aof_buf 緩衝區中的內容寫入並同步到 AOF 檔案。(效能最低,安全最高)
  • everysec: 將 aof_buf 緩衝區中的內容寫入到 AOF 檔案,如果上次同步 AOF 檔案的時間距離現在超過一秒鐘,那麼再次對 AOF 檔案同步,並且這個同步是由一個執行緒專門負責的。(同時兼顧效能與安全,推薦)
  • no: 將 aof_buf 緩衝區中的內容寫入到 AOF 檔案,但並不負責對 AOF 檔案的同步,把同步的控制權交由作業系統控制。(效能最高,安全最低)

以上就是 AOF 持久化的基本過程。

資料載入

由於命令資料是以協議格式儲存至檔案中的,所以在啟動 Redis 服務時檢測到 AOF 檔案的存在後會啟動載入程式。(如果 RDB 和 AOF 持久化的檔案同時存在則會優先載入 AOF 檔案資料)

啟動載入程式後,其載入過程如下圖所示:

AOF 重寫

在前面,我們提到 AOF 的這種機制會造成 AOF 資料檔案越來越大,並且可能會存在許多無意義的命令。例如,先執行了一個命令 set chang xuan ,隨後又執行了命令 del chang 。其實這兩條語句都會被持久化到 AOF 檔案中,但實際上除了能證明曾經執行過這兩條命令之外對於我們要持久化資料的目的而言並沒有什麼作用。

對此,Redis 提供了 AOF 重寫的機制。

Redis 的 AOF 重寫其實是根據當前儲存的資料,生成命令的過程。並且會採用一些策略儘量減小 AOF 檔案的大小,例如對於 List 中的資料會盡量使用較少的命令操作較多的資料。當然,如果在當前程式中進行重寫處理並且資料量特別大的情況下肯定會阻塞客戶端的請求,所以和 RDB 一樣,Redis 提供了 AOF 後臺重寫的機制。

後臺重寫(BGREWRITEAOF)

AOF 通過 fork 子程式的方式進行後臺重寫有兩個優點:

  1. 重寫期間伺服器程式可以繼續處理請求。
  2. 子程式帶有伺服器程式的資料副本,能充分利用作業系統提供的寫時複製機制從而提升效率,還可以在避免使用鎖的情況下保證資料的安全性。

天下沒有免費的午餐,這種方式還帶來一個問題。就是在使用子程式重寫期間,如果父程式還在處理著客戶端請求,如何保證重寫後 AOF 檔案資料的一致性呢?

對於這個問題,Redis 設定了一個 AOF 重寫緩衝區。在子程式被建立後,Redis 伺服器就會啟用這個重寫緩衝區。在將命令以協議格式追加到 AOF 緩衝區之後,同時也會追加到 AOF 重寫緩衝區。

當子程式完成重寫工作後會向父程式傳送一個訊號。父程式接收到信後之後會進行呼叫相關函式,進行以下工作:

  1. 將 AOF 重寫緩衝區中的內容寫入到新的 AOF 檔案中。
  2. 對新的 AOF 檔案進行改名,原子地覆蓋現有的 AOF 檔案,完成新舊檔案的替換。

這時,就完成了一次 AOF 後臺重寫。

總結

通過前文內容,我們可以大致清楚 Redis 所提供的 RDB 和 AOF 兩種持久化機制的過程以及基本原理。它們各有特點,也各有適合使用的場景所以並不能說誰一定比誰好。通過搭配使用,能夠確保線上環境資料的安全性就是最好的。

相關文章