Redis保證數據可靠性的策略


一個可靠安全的系統,肯定要考慮數據的可靠性,尤其對於內存為主的redis,就要考慮一旦服務器掛掉,啟動之后,如何恢復數據的問題,也就是說數據如何持久化的問題。redis保證數據的可靠性主要有兩種策略:RDB,AOF.

1.RDB

redis以數據結構的形式將數據存放在內存中,為了讓數據在redis服務器掛掉重啟之后可以繼續服務,那么就必須對數據進行持久化處理。

1.1 RDB文件格式

RDB文件格式如下所示:

  • REDIS——開始REDIS 五個字符,標識着一個 RDB 文件的開始
  • RDB-VERSION——一個四字節長的以字符表示的整數,記錄了該文件所使用的 RDB 版本號。 不同版本的RDB文件是不兼容的,因此在載入RDB文件的時候需要選擇RDB文件的版本號。
  • DB-DATA ——保存內存快照部分。
  • EOF——結束標志位
  • CHECK-SUM——redis文件所有內容的校驗和。REDIS 在寫入 RDB 文件時將校驗和保存在 RDB 文件的末尾,當讀取時,根據它的值對內容
    進行校驗。 如果為0,則表示已經關閉了檢查功能。

1.2 RDB方式介紹

RDB方式:redis將內存中的數據庫快照保存到磁盤中。redis服務器在掛掉重啟之后,可以通過加載RDB文件進行數據的恢復。可以繼續提供服務。redis提供了rdbSave ,redbLoad,其中rdbSave 是將內存中的快照以RDB格式保存到磁盤中。redbLoad是在redis重啟后將磁盤中的RDB文件加載到內存中恢復數據。

  • 保存

  rdbSave 將內存中的快照以RDB格式保存到磁盤中,如果RDB文件存在,則覆蓋為最新的RDB文件。在保存RDB文件期間,主線程需要被阻塞,知道保存完成為止。其中SAVE和BGSAVE 都會調用  rdbSave 函數,但是處理方式有一些區別:

     SAVE 直接調用 rdbSave ,阻塞 Redis 主進程,直到保存完成為止。在主進程阻塞期間,服務器不能處理客戶端的任何請求。

     BGSAVE fork 出一個子進程,子進程負責調用 rdbSave ,並在保存完成之后向主進程發送信號,通知保存已完成。因為 rdbSave 在子進程被調用,所以 Redis 服務器在BGSAVE 執行期間仍然可以繼續處理客戶端的請求 。

  • 加載

  redbLoad函數用於在redis重啟后將磁盤中的RDB文件加載到內存中恢復數據。在載入期間,服務器每載入 1000 個鍵就處理一次所有已到達的請求,不過只有 PUBLISH SUBSCRIBE PSUBSCRIBE UNSUBSCRIBE PUNSUBSCRIBE 五個命令的請求會被正確地處理,
  其他命令一律返回錯誤。等到載入完成之后,服務器才會開始正常處理所有命令。
  Note: 發布與訂閱功能和其他數據庫功能是完全隔離的,前者不寫入也不讀取數據庫,所以在服務器載入期間,訂閱與發布功能仍然可以正常使用,而不必擔心對載入數據的完整性產生影響。

  另外,因為 AOF 文件的保存頻率通常要高於 RDB 文件保存的頻率,所以一般來說,AOF 文件中的數據會比 RDB 文件中的數據要新。
  因此,如果服務器在啟動時,打開了 AOF 功能,那么程序優先使用 AOF 文件來還原數據。只有在 AOF 功能未打開的情況下,Redis 才會使用 RDB 文件來還原數據。

2.AOF

2.1 AOF同步redis寫操作

為了保證redis數據的可靠性,采用了另一種方式AOF同步,將redis寫操作命令及參數保存到AOF文件中。除了SELECT命令是是AOF自己加上去的,其他命令都是之前客戶端調用發來的寫操作命令,同步命令到AOF文件包括三個流程:

  • 命令傳播:寫操作命令包括參數的個數,命令的參數發送到AOF程序中。
  • 緩存追加:AOF程序將接收到的寫操作信息,轉換為網絡通訊協議的格式,然后將其追加到AOF緩存中。
  • 文件寫入和保存:AOF緩存中的文件被寫入到AOF文件末尾,如果設定的AOF保存條件被滿足的話,fsync 函數或者 fdatasync 函數會被調用,將寫入的內容真正地保存
    到磁盤中 。

 

下面來詳細介紹下這三個步驟。

2.2 命令傳播

當一個 Redis 客戶端需要執行命令時 ,他通過網絡協議將命令操作傳給redis服務器,比 如 說, 要 執 行 命 令 SET KEY VALUE , 客 戶 端 將 向 服 務 器 發 送 文 本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"

服務器接收到文本之后將其轉化為redis字符串對象。比如 SET KEY VALUE,redis服務器向指向實現 SET 命令的setCommand 函數 ,創建三個字符串SET,KEY,VALUE,分別保存 SET KEY VALUE 三個參數(命
令也算作參數)。
2.3緩存追加
現在命令傳播過程已經准備好了AOF程序中可用的redis字符串,現在需要將將命令還原成 Redis 網絡通訊協議 ,然后將協議文本追加到aof_buf字符創對象中。

redisServer 結構維持着 Redis 服務器的狀態,aof_buf 域則保存着所有等待寫入到 AOF
件的協議文本:
struct redisServer {
// 其他域...
sds aof_buf;
// 其他域...
};

2.4文件寫入和保存

每當服務器常規任務函數被執行、或者事件處理器被執行時,aof.c/flushAppendOnlyFile 函數都會被調用,這個函數執行以下兩個工作:
WRITE:根據條件,將 aof_buf 中的緩存寫入到 AOF 文件。
SAVE:根據條件,調用 fsync fdatasync 函數,將 AOF
文件保存到磁盤中

2.5引發的問題

到此為止,我們就看到了AOF保證redis數據可靠性的方案。但是問題是,隨着我們業務量不斷增大,我們的AOF文件也會越來越龐大,越來越臃腫,這對於以內存為基礎的redis數據庫而言

是致命的傷害。那么有什么辦法可以解決掉這個問題呢?

答案就是AOF文件重寫,當然這個重寫不是說重寫原來的AOF文件,而是新建一個AOF文件,寫入當前數據庫狀態來實現的。

比如:RPUSH list 1 2 3 4      // [1, 2, 3, 4]
RPOP list            // [1, 2, 3]
LPOP list        // [2, 3]
LPUSH list 1     // [1, 2, 3]
如果執行了着一些系列redis命令之后,那原來的AOF文件中必然追加了四次這樣的協議文本。但是如果我們通過重寫當前數據框狀態的方式就只需要寫入數據庫中當前此鍵的狀態RPUSH list 1 2 3       // [1, 2, 3 ] 即可。

根據鍵的類型,使用適當的寫入命令來重現鍵的當前值,這就是 AOF 重寫的實現原理。整個重寫過程可以用偽代碼表示如下:

def AOF_REWRITE(tmp_tile_name):
f = create(tmp_tile_name)
# 遍歷所有數據庫 for db in redisServer.db:
# 如果數據庫為空,那么跳過這個數據庫 if db.is_empty(): continue # 寫入 SELECT 命令,用於切換數據庫
f.write_command("SELECT " + db.number)
# 遍歷所有鍵 for key in db:
# 如果鍵帶有過期時間,並且已經過期,那么跳過這個鍵 if key.have_expire_time() and key.is_expired(): continue
if key.type == String:
# 用 SET key value 命令來保存字符串鍵
value = get_value_from_string(key)
f.write_command("SET " + key + value)
elif key.type == List:
# 用 RPUSH key item1 item2 ... itemN 命令來保存列表鍵
item1, item2, ..., itemN = get_item_from_list(key)
f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)
elif key.type == Set:
# 用 SADD key member1 member2 ... memberN 命令來保存集合鍵
member1, member2, ..., memberN = get_member_from_set(key)
f.write_command("SADD " + key + member1 + member2 + ... + memberN)
elif key.type == Hash:
# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令來保存哈希鍵
field1, value1, field2, value2, ..., fieldN, valueN =\
get_field_and_value_from_hash(key)
f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
... + fieldN + valueN)
elif key.type == SortedSet:
# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN # 命令來保存有序集鍵
score1, member1, score2, member2, ..., scoreN, memberN = \
get_score_and_member_from_sorted_set(key)
f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
... + scoreN + memberN)
else:
raise_type_error()
# 如果鍵帶有過期時間,那么用 EXPIREAT key time 命令來保存鍵的過期時間 if key.have_expire_time():
f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())
# 關閉文件
f.close()

根據偽代碼可以看出,redis重寫不僅有利於釋放原來龐大的AOF文件占用的內存,而且還能清除過期鍵所占用的內存。

 現在我們知道了AOF文件可以創建一個新的AOF文件,並保存數據庫當前狀態,會存在大量的寫操作命令,所以代用這個函數的調用者會被阻塞很長一段時間,這是因為redis使用單線程來處理命令請求的。這個時候,redis服務器無法處理客戶端發來的任何請求,這肯定不是我們希望的。所以

Redis 決定將 AOF 重寫程序放到(后台)子進程里執行,這樣處理的最大好處是:

  • 子進程進行AOF重寫的時候,主線程可以處理客戶端發來的命令請求。
  • 子進程帶有主進程的數據副本,使用子進程而不是線程,可以在避免鎖的情況下,保證數
    據的安全性。

2.6 子進程引發的另一個問題

redis 處理請求時單線程的。通過開啟子線程來進行AOF文件重寫,能夠解決主線程被阻塞的問題。但是,由於主線程在子線程進行AOF文件重寫的時候,繼續處理客戶端的請求,有可能對已經寫入的AOF文件的命令做了修改,這個時候AOF文件寫入的命令還是原來的,狀態就不一致了。

比如說:我在1s的時候AOF重寫命令RPUSH list 1 2 3       // [1, 2, 3 ] ,1s后主線程又接收了一個客戶端命令請求:LPUSH list 1     // [1, 2, 3,4]。此時我們可以看到redis數據庫與重寫的AOF文件狀態不一致。

為了解決這個問題,Redis 增加了一個 AOF 重寫緩存。這個緩存在fork出子線程之后開啟應用,redis在接收到寫操作命令式,一方面將寫操作命令傳入到AOF重寫緩存中,另一方面將寫操作命令追加到當前重寫的AOF文件中。就是說,主進程在開啟了子進程進行AOF重寫的時候做了

一下三件事:

  • 處理命令請求
  • 寫操作命令追加到重寫的AOF文件中
  • 寫操作命令追加到AOF重寫緩存中

這樣一來就可以保證,在現有AOF程序繼續執行,一旦在AOF執行期間宕機,也不會發生數據丟失。一方面又保證了所有對數據庫的寫操作命令到在AOF緩存中,有利於redis服務器中數據與AOF數據不一直的問題。

那么接下來要討論等寫操作命令寫到了AOF緩存中,接下來如何處理呢?

當子進程完成AOF重寫命令,就會向主進程發送一個成信號,父進程在接到完成信號之后,會調用一個信號處理函數,並完成以下工作:

  •  AOF 重寫緩存中的內容全部寫入到新 AOF 文件中
  • 新的AOF文件覆蓋掉原來的AOF文件。

在整個 AOF后台重寫過程中,只有最后的寫入緩存和改名操作會造成主進程阻塞,在其他時候,AOF 后台重寫都不會對主進程造成阻塞,這將 AOF 重寫對性能造成的影響降到了最低。

2.7 什么時候出發AOF后台程序?

那么問題來了,什么時候fork一個子程序進行AOF重寫呢?就是說在什么樣的情況下,需要AOF重寫呢?有兩種方案

  • AOF 重寫可以由用戶通過調用 BGREWRITEAOF 手動觸發
  • AOF重寫可以通過配置設置成為自動出發。服務器在 AOF 功能開啟的情況下,會維持 三個變量:錄當前 AOF 文件大小的變量 aof_current_size,記錄最后一次 AOF 重寫之后,AOF 文件大小的變量 aof_rewirte_base_size,增長百分比變量 aof_rewirte_perc 。

  每次當 serverCron 函數執行時,它都會檢查以下條件是否全部滿足,如果是的話,就會觸發自動的 AOF 重寫 :
    1. 沒有 BGSAVE 命令在進行。
    2. 沒有 BGREWRITEAOF 在進行。
    3. 當前 AOF 文件大小大於 server.aof_rewrite_min_size (默認值為 1 MB)。
    4. 當前 AOF 文件大小和最后一次 AOF 重寫后的大小之間的比率大於等於指定的增長百分比。
  默認情況下,增長百分比為 100% ,也即是說,如果前面三個條件都已經滿足,並且當前 AOF文件大小比最后一次 AOF 重寫時的大小要大一倍的話,那么觸發自動 AOF 重寫

2.8總結

來總結一下,這里主要講了幾個問題:

  • redis數據庫為了保證其數據可靠性,采用RDB和AOF方式來同步數據庫狀態。
  • RDB方式將redis數據庫快照保存到磁盤中,一旦重啟redis服務器,只需要到磁盤中加載數據庫快照即可。AOF方式將redis執行的寫操作命令追加到AOF文件中,一旦重啟redis服務器,只需要執行AOF文件中的命令,即可還原服務器中數據庫的狀態。
  • AOF為了避免隨着業務壯大導致的AOF文件越來越龐大,占內存空間的問題,采用AOF重寫機制,即只同步數據庫的當前狀態到新的AOF文件中,然后以新的AOF文件覆蓋舊的AOF文件。
  • redis數據庫是單線程的,這一點要務必牢記。所以在主線程處理客戶端寫操作命令請求,一方面又需要AOF重寫,這樣會導致主線程阻塞。為了解決這個問題,設計了AOF緩存,將主線程新的寫操作命令追加到AOF緩存中,一旦AOF重寫完成,發送一個完成命令給

  主線程,則主線程繼續講AOF緩存中的寫操作命令同步到當前新的AOF文件中,並用新的AOF文件替代掉舊的AOF文件。

  • AOF重寫既 可以由用戶手動啟動,也可以由服務器自動開啟。AOFAOF 可以由用戶手動觸發,也可以由服務器自動觸發 可以由用戶手動觸發,也可以由服務器自動觸發

 

 

 

 

 

 

      

 


免責聲明!

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



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