Redis | 第5章 Redis 中的持久化技術《Redis設計與實現》


前言

參考資料:《Redis設計與實現 第二版》;

第二部分為單機資料庫的實現,主要由以下模組組成:資料庫持久化事件客戶端伺服器

本篇將介紹 Redis 中的持久化技術,主要有兩種:RDB持久化AOF持久化


1. RDB 持久化

1.1 RDB 文件的創建與載入

  • Redis使用 SAVEBGSAVE 命令生成 RDB 文件;
    • SAVE:會阻塞 Redis 伺服器進程,直到 RDB 文件創建完畢為止,阻塞期間伺服器不能處理任何命令請求;
    • BGSAVE:會派生一個子進程,由指進程負責創建 RDB 文件,父進程繼續處理命令請求。BGSAVE 執行期間,會發生以下特殊情況:
      • BGSAVE 命令執行期間,客戶端發送 SAVEBGSAVE 命令會被伺服器拒絕,防止產生競爭條件。客戶端發送 BGREWRITEAOF 命令會被延遲;
      • BGREWRITEAOF 命令執行期間,客戶端發送 BGSAVE 命令會被伺服器拒絕;
  • 創建 RDB 文件由 rdb.c/rdbSave 函數完成;
  • 載入 RDB 文件由 rdb.c/rdbLoad 函數完成;
  • RDB 文件的載入工作是在伺服器啟動時自動執行,只要 Redis 伺服器在啟動時檢測到 RDB 文件存在,就會自動載入 RDB 文件;
  • AOF 文件的更新頻率通常比 RDB 文件更新頻率高:
    • 當伺服器開啟了 AOF 持久化功能時,會優先使用 AOF 文件還原資料庫狀態;
    • 當伺服器關閉了 AOF 持久化功能時,才會使用 RDB 文件來還原資料庫狀態;
  • 伺服器在載入 RDB 文件期間,會一直處於阻塞狀態,直到載入工作完成為止;

1.2 自動間隔性保存

1.2.1 設置保存條件

  • 伺服器會根據 save 選項所設置的保存值,設置伺服器狀態 redisServer結構的 saveparams 屬性;

  • redisServer 的結構定義:

    struct redisServer{
        //...
        //記錄了保存條件的數組
        struct saveparam *saveparams;
        //
    }
    
  • saveparams 屬性是一個數組,每個 saveparam 結構保存了一個 save 設置的保存條件;

  • saveparam 的結構定義:

    struct saveparam{
        //秒數
        time_t seconds;
        //修改值
        int changes;
    }
    

saveparam屬性

1.2.2 dirty 計數器和 lastsave 屬性

  • dirty 屬性和 lastsave 屬性在 redisServer 結構體里:

    struct redisServer{
        //...
        //修改計數器
        long long dirty;
        //上一次執行保存的時間
        time_t lastsave;
    };
    
    • dirty 計數器記錄距離上一次成功執行 SAVE 命令或者 BGNAME 命令之後,伺服器對資料庫狀態進行了多少次修改;
    • lastsave 屬性是一個 UNIX 時間戳,記錄了伺服器上一次成功執行 SAVE 命令或 BGSAVE 命令的時間;

1.2.3 檢查保存條件是否滿足

  • Redis 的伺服器周期性操作函數 serverCron 默認每隔 100ms 會執行一次,其中包括檢查 save 選項所設置的保存條件是否滿足(遍歷並檢查 saveparams 數組中的所有保存條件),滿足則執行 BGSAVE 命令;

1.3 RDB 文件

1.3.1 RDB 的文件結構

  • RDB 文件結構的邏輯圖:
    RDB文件結構

  • 各個欄位含義:

    欄位 長度 儲存值 說明
    REDIS 5位元組 「REDIS」 在載入文件時,快速檢查所載入的文件是否為 RDB 文件
    db_version 4位元組 字元串表示的整數 RDB 文件的版本號
    databases 0個或任意多個資料庫,以及資料庫中的鍵值對數據
    EOP 1位元組 EOP 常量 表示 RDB 文件正文內容的結束
    check_sum 8位元組 無符號整數 前4個部分的校驗和

1.3.2 database 的文件結構

  • database 為 RDB 文件的結構組成部分;
  • databases 部分的邏輯結構:

databases部分的邏輯結構

  • 各欄位含義:

    欄位 長度 存儲值 說明
    SELECTDB 1位元組 常量 表示接下來讀入資料庫號碼
    db_number 1、2或5位元組 數字 表示資料庫號碼
    key_value_pairs 長度不定 資料庫所有的鍵值對數據

1.3.3 key_value_pairs 的文件結構

  • key_value_pairs 為 databases 的結構組成部分;有兩種類型,一種不帶過期時間,一種帶過期時間;
  • key_value_pairs 部分的邏輯結構:
    不帶過期時間的鍵值對

帶有過期時間的鍵值對

  • 各欄位含義:

    欄位 長度 存儲值 說明
    EXPIRETIME_MS 1位元組 數值 表示過期時間
    ms 8位元組 數值 以毫秒為單位的 UNIX 時間戳
    TYPE 1位元組 常量 代表一種對象類型或底層編碼
    key 長度不定 字元串對象 表示鍵對象
    value 長度不定 各種對象 表示值對象

1.3.4 value 的編碼

  • value 為 key_value_pairs 的結構組成成分;
  • value 值對象的結構和長度會根據 TYPE 類型的不同而不同;
  • value可以是字元串對象列表對象集合對象哈希表對象有序集合對象INTSET編碼的集合ZIPLIST編碼的列表、哈希表或有序集合
  • value的格式與編碼對應請見 《第3章 對象》1.1 對象的定義
  • 字元串對象的格式與示例:
  • 字元串對象可分為:壓縮字元串無壓縮字元串兩種:

壓縮字元串對象的value格式

字元串對象的value示例

  • 列表與集合對象的格式:
    列表與集合對象的value格式

列表與集合對象的value示例

  • 哈希表對象的格式:

哈希表對象的value格式
哈希表對象的value示例

  • 有序集合對象的格式:

有序集合對象的value格式
有序集合對象的value示例

  • INTSET 編碼集合的格式:

    • 將整數集合轉換成字元串即可;
  • ZIPLIST編碼的列表、哈希表或有序集合的格式:

    • 將壓縮列錶轉換成一個字元串對象,然後再保存到 RDB 文件;

1.4 RDB 文件的示例

  • 不包含任何鍵值對的 RDB 文件:

    REDIS標識 db_version EOF標識 check_num
    REDIS 0006 377 334 263 c 360 z 334 362 v
  • 包含字元串鍵的 RDB 文件:

    REDIS標識 db_version SELECTDB db_number,0 號資料庫 TYPE,\0 表示字元串 key value EOF標識 check_num
    REDIS 0006 376 \0 \0 003 MSG 005 HELLO 377 207 z = 304 f T L 343
  • 包含帶有過期時間的字元串鍵的 RDB 文件:

    REDIS標識 db_version SELECTDB 切換資料庫 EXPIRETIME_MS ms TYPE,\0 表示字元串 key value EOF標識 check_num
    REDIS 0006 376 \0 374 \ 2 365 336 @ 001 \0 \0 \0 003 MSG 005 HELLO 377 212 231 x 247 252 } 021 306
  • 包含一個集合鍵的 RDB 文件:

    REDIS標識 db_version SELECTDB 切換資料庫 常量 REDIS_RDB_TYPE_SET key 集合大小 第一個元素 第二個元素 第三個元素 EOF常量 check_num
    REDIS 0006 376 \0 002 004 LANG 003 004 RUBY 004 JAVA 001 C 377 202 312 r 352 346 305 * 023

2 AOF 持久化與 RDB 持久化的區別

  • AOF 持久化:保存 Redis 伺服器所執行的命令來記錄資料庫狀態;
  • RDB 持久化:保存資料庫中的鍵值對來記錄資料庫狀態不同;

3. AOF 持久化

3.1 AOF 持久化的實現

  • AOF 持久化功能可分為:追加(append)、文件寫入、文件同步(sync)三個步驟;

  • AOF 文件中的所有命令都以 Redis 命令請求協議的格式保存;

  • 當 AOF 持久化功能打開時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到伺服器狀態的 aof_buf 緩衝區的末尾:

    struct redisServer{
        //...
        //AOF 緩衝區
        sds aof_buf;
    };
    
  • AOF 文件的寫入與同步依賴事件循環 loop,每次循環主要有三個工作:

    • 處理文件事件:負責接收客戶端的命令請求,以及向客戶端發送命令回復;
    • 處理時間事件:執行需要定時運行的函數;
    • flushAppendOnlyFile():考慮是否將 aof_buf 中的內容追加到 AOF 文件中;
  • flushAppendOnlyFile() 函數的行為由伺服器配置的 appendfsync 選項的值決定,該值有三種不同的行為:

    appendfsync 選項的值 flushAppendOnlyFile 函數的行為 效率與安全性
    always 將 aof_buf 緩衝區中的所有內容寫入並同步到 AOF 文件 效率最慢,安全性最高
    everysec 將 aof_buf 緩衝區中的所有內容寫入並同步到 AOF 文件,如果上次同步 AOF 文件的事件距離現在超過 1s ,則對再次 AOF 文件進行同步,並且這個同步由一個執行緒專門負責 效率高
    no 將 aof_buf 緩衝區中的所有內容寫入到 AOF 文件,但不對 AOF 文件進行同步,何時同步由作業系統決定 效率最高,安全性最低

3.2 AOF 文件的載入與數據還原

  • 伺服器創建一個不帶網路連接的偽客戶(fake client),偽客戶端讀入並執行 AOF 文件即可;

AOF 文件的載入與數據還原

3.3 AOF 重寫

  • AOF 重寫不需要對現有 AOF 文件進行任何讀取、分析或寫入操作,而是通過讀取伺服器當前資料庫狀態實現;
  • AOF 重寫功能的實現原理:從資料庫讀取鍵現在的值,然後用一條命令記錄鍵值對,代替之前記錄這個鍵值對的多條命令
  • 為了避免執行命令時造成客戶端輸入緩衝區溢出,重寫程式在處理列表、哈希表、集合、有序集合這四種鍵時,會檢查元素數量,超過一定數量(64)時會使用多條命令記錄這個鍵的情況;
    • 這個數量由常量 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 確定;

3.4 AOF 後台重寫

  • AOF 重寫需要解決2個問題:
    • 重寫不能阻礙伺服器處理客戶端請求:使用子進程解決;
    • 子進程在 AOF 重寫期間,父進程伺服器對資料庫狀態進行修改,會使伺服器當前狀態與重寫後 AOF 狀態不一致:設置AOF 重寫緩衝區解決;
  • Redis 將 AOF 重寫程式放到子進程里執行:
    • 子進程進行 AOF 重寫期間,伺服器進程(父進程)可以繼續處理命令請求;
    • 子進程帶有伺服器進程的數據副本,使用子進程而不是執行緒,避免使用鎖的情況下保證數據安全;

AOF 文件重寫時的伺服器進程與子進程

  • Redis 伺服器設置一個 AOF 重寫緩衝區,以保證:

    • AOF 緩衝區的內容會定期被寫入和同步到 AOF 文件,對現有 AOF 文件的處理工作如常進行;
    • 從創建子進程開始,伺服器執行的所有寫命令都會被記錄到 AOF 重寫緩衝區里;
  • 子進程完成 AOF 重寫工作後,向父進程發送一個訊號,父進程接到訊號後調用訊號處理函數,執行以下工作:

    • 將 AOF 重寫緩衝區中的所有內容寫入到新 AOF 文件,此時新 AOF 文件保存的資料庫狀態將與伺服器當前的資料庫狀態一致;
    • 對新的 AOF 文件進行改名,原子地覆蓋現有的 AOF 文件,完成新舊兩個 AOF 文件的替換;
  • 只有號處理函數執行時會對伺服器進程(父進程)造成阻塞;

AOF 文件後台重寫過程


最後

新人製作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!