天天看點

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

文章目錄

  • 一、RDB持久化
    • 1、RDB檔案的建立與載入
      • 1.1 SAVE指令執行時的伺服器狀态
      • 1.2 BGSAVE指令執行時的伺服器狀态
      • 1.3 RDB檔案載入時的伺服器狀态
    • 2、自動間隔性儲存
      • 2.1 設定儲存條件
      • 2.2 dirty計數器和lastsave屬性
      • 2.3 檢查儲存條件是否滿足
    • 3、RDB檔案結構
      • 3.1 databases部分
      • 3.2 key_value_pairs部分
      • 3.3 value的編碼
    • 4、重點回顧
  • 二、AOF持久化
    • 1、AOF持久化的實作
      • 1.1 指令追加
      • 1.2 AOF檔案的寫入與同步
    • 2、AOF檔案的載入與資料還原
    • 3、AOF重寫
      • 3.1 AOF檔案重寫的實作
      • 3.2 AOF背景重寫
    • 4、重點回顧
  • 三、事件
    • 1、檔案事件
      • 1.1 檔案事件處理器的構成
      • 1.2 I/O多路複用程式的實作
      • 1.3 事件的類型
      • 1.4 檔案事件的處理器
    • 2、時間事件
      • 2.1 實作
      • 2.2 時間事件應用執行個體:serverCron函數
    • 3、事件的排程與執行
    • 4、重點回顧

一、RDB持久化

Redis是一個鍵值對資料庫伺服器,伺服器中通常包含着任意個非空資料庫,而每個非空資料庫中又可以包含任意個鍵值對,為了友善起見,我們将伺服器中的非空資料庫以及它們的鍵值對統稱為資料庫狀态。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

因為Redis是記憶體資料庫,它将自己的資料庫狀态儲存在記憶體裡面,是以如果不想辦法将儲存在記憶體中的資料庫狀态儲存到磁盤裡面,那麼一旦伺服器程序退出,伺服器中的資料庫狀态也會消失不見。

為了解決這個問題,Redis提供了RDB持久化功能,這個功能可以将Redis在記憶體中的資料庫狀态儲存到磁盤裡面,避免資料意外丢失。

RDB持久化既可以手動執行,也可以根據伺服器配置選項定期執行,該功能可以将某個時間點上的資料庫狀态儲存到一個RDB檔案中。

RDB持久化功能所生成的RDB檔案是一個經過壓縮的二進制檔案,通過該檔案可以還原生成RDB檔案時的資料庫狀态。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

1、RDB檔案的建立與載入

有兩個Redis指令可以用于生成RDB檔案,一個是SAVE,另一個是BGSAVE。

SAVE指令會阻塞Redis伺服器程序,直到RDB檔案建立完畢為止,在伺服器程序阻塞期間,伺服器不能處理任何指令請求:

redis> SAVE       //等待直到RDB檔案建立完畢
OK
           

和SAVE指令直接阻塞伺服器程序的做法不同,BGSAVE指令會派生出一個子程序,然後由子程序負責建立RDB檔案,伺服器程序(父程序)繼續處理指令請求:

redis> BGSAVE     //派生子程序,并由子程序建立RDB檔案
Background saving started
           

建立RDB檔案的實際工作由rdb.c/rdbSave函數完成,SAVE指令和BGSAVE指令會以不同的方式調用這個函數,通過以下僞代碼可以明顯地看出這兩個指令之間的差別:

def SAVE():
    # 建立RDB檔案
    rdbSave()
        
def BGSAVE():
    # 建立子程序
    pid = fork()
    if pid == 0:
        # 子程序負責建立RDB檔案
        rdbSave()
        # 完成之後向父程序發送信号
        signal_parent()
    elif pid > 0:
        # 父程序繼續處理指令請求,并通過輪詢等待子程序的信号
        handle_request_and_wait_signal()
    else:
        # 處理出錯情況
        handle_fork_error()
           

和使用SAVE指令或者BGSAVE指令建立RDB檔案不同,RDB檔案的載入工作是在伺服器啟動時自動執行的,是以Redis并沒有專門用于載入RDB檔案的指令,隻要Redis伺服器在啟動時檢測到RDB檔案存在,它就會自動載入RDB檔案。

$ redis-server
[7379] 30 Aug 21:07:01.270 # Server started, Redis version 2.9.11
[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds
[7379] 30 Aug 21:07:01.289 * The server is now ready to accept connections on port 6379
           

因為AOF檔案的更新頻率通常比RDB檔案的更新頻率高,是以:

  • 如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案來還原資料庫狀态。
  • 隻有在AOF持久化功能處于關閉狀态時,伺服器才會使用RDB檔案來還原資料庫狀态。
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

載入RDB檔案的實際工作由rdb.c/rdbLoad函數完成,這個函數和rdbSave函數之間的關系:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

1.1 SAVE指令執行時的伺服器狀态

當SAVE指令執行時,Redis伺服器會被阻塞,是以當SAVE指令正在執行時,用戶端發送的所有指令請求都會被拒絕。隻有在伺服器執行完SAVE指令、重新開始接受指令請求之後,用戶端發送的指令才會被處理。

1.2 BGSAVE指令執行時的伺服器狀态

BGSAVE指令的儲存工作是由子程序執行的,是以在子程序建立RDB檔案的過程中,Redis伺服器仍然可以繼續處理用戶端的指令請求,但是,在BGSAVE指令執行期間,伺服器處理SAVE、BGSAVE、BGREWRITEAOF三個指令的方式會和平時有所不同。

  • 在BGSAVE指令執行期間,用戶端發送的SAVE指令會被伺服器拒絕,伺服器禁止SAVE指令和BGSAVE指令同時執行是為了避免父程序(伺服器程序)和子程序同時執行兩個rdbSave調用,防止産生競争條件。
  • 在BGSAVE指令執行期間,用戶端發送的BGSAVE指令會被伺服器拒絕,因為同時執行兩個BGSAVE指令也會産生競争條件。
  • 最後,BGREWRITEAOF和BGSAVE兩個指令不能同時執行:
    • 如果BGSAVE指令正在執行,那麼用戶端發送的BGREWRITEAOF指令會被延遲到BGSAVE指令執行完畢之後執行。
    • 如果BGREWRITEAOF指令正在執行,那麼用戶端發送的BGSAVE指令會被伺服器拒絕。

1.3 RDB檔案載入時的伺服器狀态

伺服器在載入RDB檔案期間,會一直處于阻塞狀态,直到載入工作完成為止。

2、自動間隔性儲存

SAVE和BGSAVE的主要差別:SAVE指令由伺服器程序執行儲存工作,BGSAVE指令則由子程序執行儲存工作,是以SAVE指令會阻塞伺服器,而BGSAVE指令則不會。

因為BGSAVE指令可以在不阻塞伺服器程序的情況下執行,是以Redis允許使用者通過設定伺服器配置的save選項,讓伺服器每隔一段時間自動執行一次BGSAVE指令。

使用者可以通過save選項設定多個儲存條件,但隻要其中任意一個條件被滿足,伺服器就會執行BGSAVE指令。

save 900 1
save 300 10
save 60 10000
           

那麼隻要滿足以下三個條件中的任意一個,BGSAVE指令就會被執行:

  • 伺服器在900秒之内,對資料庫進行了至少1次修改。
  • 伺服器在300秒之内,對資料庫進行了至少10次修改。
  • 伺服器在60秒之内,對資料庫進行了至少10000次修改。
[5085] 03 Sep 17:09:49.463 * 10000 changes in 60 seconds. Saving...
[5085] 03 Sep 17:09:49.463 * Background saving started by pid 5189
[5189] 03 Sep 17:09:49.522 * DB saved on disk
[5189] 03 Sep 17:09:49.522 * RDB: 0 MB of memory used by copy-on-write
[5085] 03 Sep 17:09:49.563 * Background saving terminated with success
           

2.1 設定儲存條件

當Redis伺服器啟動時,使用者可以通過指定配置檔案或者傳入啟動參數的方式設定save選項,如果使用者沒有主動設定save選項,那麼伺服器會為save選項設定預設條件:

save 900 1
save 300 10
save 60 10000
           

伺服器程式會根據save選項所設定的儲存條件,設定伺服器狀态redisServer結構的saveparams屬性:

struct redisServer {
    // ...
    // 記錄了儲存條件的數組
    struct saveparam *saveparams;
    // ...
};
           

saveparams屬性是一個數組,數組中的每個元素都是一個saveparam結構,每個saveparam結構都儲存了一個save選項設定的儲存條件:

struct saveparam {
    // 秒數
    time_t seconds;
    // 修改數
    int changes;
};
           
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

2.2 dirty計數器和lastsave屬性

除了saveparams數組之外,伺服器狀态還維持着一個dirty計數器,以及一個lastsave屬性:

  • dirty計數器記錄距離上一次成功執行SAVE指令或者BGSAVE指令之後,伺服器對資料庫狀态(伺服器中的所有資料庫)進行了多少次修改(包括寫入、删除、更新等操作)。
  • lastsave屬性是一個UNIX時間戳,記錄了伺服器上一次成功執行SAVE指令或者BGSAVE指令的時間。
struct redisServer {
    // ...
    // 修改計數器
    long long dirty;
    // 上一次執行儲存的時間
    time_t lastsave;
    // ...
};
           

當伺服器成功執行一個資料庫修改指令之後,程式就會對dirty計數器進行更新:指令修改了多少次資料庫,dirty計數器的值就增加多少。

redis> SADD database Redis MongoDB MariaDB
(integer) 3
           

程式會将dirty計數器的值增加3。

伺服器狀态中包含的dirty計數器和lastsave屬性:[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-TD7vVnVn-1621825218187)(3、Redis的持久化機制/image-20210523211834075.png)]

  • dirty計數器的值為123,表示伺服器在上次儲存之後,對資料庫狀态共進行了123次修改。
  • lastsave屬性則記錄了伺服器上次執行儲存操作的時間1378270800(2013年9月4日零時)。

2.3 檢查儲存條件是否滿足

Redis的伺服器周期性操作函數serverCron預設每隔100毫秒就會執行一次,該函數用于對正在運作的伺服器進行維護,它的其中一項工作就是檢查save選項所設定的儲存條件是否已經滿足,如果滿足的話,就執行BGSAVE指令。

def serverCron():
    # ...
    # 周遊所有儲存條件
    for saveparam in server.saveparams:
        # 計算距離上次執行儲存操作有多少秒
        save_interval = unixtime_now()-server.lastsave
        # 如果資料庫狀态的修改次數超過條件所設定的次數
        # 并且距離上次儲存的時間超過條件所設定的時間
        # 那麼執行儲存操作
        if      server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
            BGSAVE()
        # ...
           

程式會周遊并檢查saveparams數組中的所有儲存條件,隻要有任意一個條件被滿足,那麼伺服器就會執行BGSAVE指令。

目前狀态:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

當時間來到1378271101,也即是1378270800的301秒之後,伺服器将自動執行一次BGSAVE指令,因為saveparams數組的第二個儲存條件——300秒之内有至少10次修改——已經被滿足。

更新之後:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

以上就是Redis伺服器根據save選項所設定的儲存條件,自動執行BGSAVE指令,進行間隔性資料儲存的實作原理。

3、RDB檔案結構

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
  • RDB檔案的最開頭是REDIS部分,這個部分的長度為5位元組,儲存着“REDIS”五個字元。通過這五個字元,程式可以在載入檔案時,快速檢查所載入的檔案是否RDB檔案。
  • db_version長度為4位元組,它的值是一個字元串表示的整數,這個整數記錄了RDB檔案的版本号,比如"0006"就代表RDB檔案的版本為第六版。
  • databases部分包含着零個或任意多個資料庫,以及各個資料庫中的鍵值對資料:
    • 如果伺服器的資料庫狀态為空(所有資料庫都是空的),那麼這個部分也為空,長度為0位元組。
    • 如果伺服器的資料庫狀态為非空(有至少一個資料庫非空),那麼這個部分也為非空,根據資料庫所儲存鍵值對的數量、類型和内容不同,這個部分的長度也會有所不同。
  • EOF常量的長度為1位元組,這個常量标志着RDB檔案正文内容的結束,當讀入程式遇到這個值的時候,它知道所有資料庫的所有鍵值對都已經載入完畢了。
  • check_sum是一個8位元組長的無符号整數,儲存着一個校驗和,這個校驗和是程式通過對REDIS、db_version、databases、EOF四個部分的内容進行計算得出的。伺服器在載入RDB檔案時,會将載入資料所計算出的校驗和與check_sum所記錄的校驗和進行對比,以此來檢查RDB檔案是否有出錯或者損壞的情況出現。

3.1 databases部分

一個RDB檔案的databases部分可以儲存任意多個非空資料庫。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

每個非空資料庫在RDB檔案中都可以儲存為SELECTDB、db_number、key_value_pairs三個部分:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
  • SELECTDB常量的長度為1位元組,當讀入程式遇到這個值的時候,它知道接下來要讀入的将是一個資料庫号碼。
  • db_number儲存着一個資料庫号碼,根據号碼的大小不同,這個部分的長度可以是1位元組、2位元組或者5位元組。當程式讀入db_number部分之後,伺服器會調用SELECT指令,根據讀入的資料庫号碼進行資料庫切換,使得之後讀入的鍵值對可以載入到正确的資料庫中。
  • key_value_pairs部分儲存了資料庫中的所有鍵值對資料,如果鍵值對帶有過期時間,那麼過期時間也會和鍵值對儲存在一起。根據鍵值對的數量、類型、内容以及是否有過期時間等條件的不同,key_value_pairs部分的長度也會有所不同。

0号資料庫的結構:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

3.2 key_value_pairs部分

RDB檔案中的每個key_value_pairs部分都儲存了一個或以上數量的鍵值對,如果鍵值對帶有過期時間的話,那麼鍵值對的過期時間也會被儲存在内。

帶過期時間的鍵值對在RDB檔案中由TYPE、key、value三部分組成

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

TYPE記錄了value的類型,長度為1位元組,值可以是以下常量的其中一個:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

以上列出的每個TYPE常量都代表了一種對象類型或者底層編碼,當伺服器讀入RDB檔案中的鍵值對資料時,程式會根據TYPE的值來決定如何讀入和解釋value的資料。

帶有過期時間的鍵值對在RDB檔案中的結構

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

帶有過期時間的鍵值對中的TYPE、key、value三個部分的意義,和前面介紹的不帶過期時間的鍵值對的TYPE、key、value三個部分的意義完全相同,至于新增的EXPIRETIME_MS和ms,它們的意義如下:

  • EXPIRETIME_MS常量的長度為1位元組,它告知讀入程式,接下來要讀入的将是一個以毫秒為機關的過期時間。
  • ms是一個8位元組長的帶符号整數,記錄着一個以毫秒為機關的UNIX時間戳,這個時間戳就是鍵值對的過期時間。

無過期時間的字元串鍵值對:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

帶有過期時間的集合鍵值對:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

3.3 value的編碼

RDB檔案中的每個value部分都儲存了一個值對象,每個值對象的類型都由與之對應的TYPE記錄,根據類型的不同,value部分的結構、長度也會有所不同。

1.字元串對象

如果TYPE的值為REDIS_RDB_TYPE_STRING,那麼value儲存的就是一個字元串對象,字元串對象的編碼可以是REDIS_ENCODING_INT或者REDIS_ENCODING_RAW。

如果字元串對象的編碼為REDIS_ENCODING_INT,那麼說明對象中儲存的是長度不超過32位的整數,這種編碼的對象将以圖10-20所示的結構儲存。

其中,ENCODING的值可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或者REDIS_RDB_ENC_INT32三個常量的其中一個,它們分别代表RDB檔案使用8位(bit)、16位或者32位來儲存整數值integer。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

如果字元串對象的編碼為REDIS_ENCODING_RAW,那麼說明對象所儲存的是一個字元串值,根據字元串長度的不同,有壓縮和不壓縮兩種方法來儲存這個字元串:

  • 如果字元串的長度小于等于20位元組,那麼這個字元串會直接被原樣儲存。
  • 如果字元串的長度大于20位元組,那麼這個字元串會被壓縮之後再儲存。

2.清單對象

如果TYPE的值為REDIS_RDB_TYPE_LIST,那麼value儲存的就是一個REDIS_ENCODING_LINKEDLIST編碼的清單對象,RDB檔案儲存這種對象的結構:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

list_length記錄了清單的長度,它記錄清單儲存了多少個項(item),讀入程式可以通過這個長度知道自己應該讀入多少個清單項。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

結構中的第一個數字3是清單的長度,之後跟着的分别是第一個清單項、第二個清單項和第三個清單項,其中:

  • 第一個清單項的長度為5,内容為字元串"hello"。
  • 第二個清單項的長度也為5,内容為字元串"world"。
  • 第三個清單項的長度為1,内容為字元串"!"。

3.集合對象

如果TYPE的值為REDIS_RDB_TYPE_SET,那麼value儲存的就是一個REDIS_ENCODING_HT編碼的集合對象,RDB檔案儲存這種對象的結構:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

set_size是集合的大小,它記錄集合儲存了多少個元素,讀入程式可以通過這個大小知道自己應該讀入多少個集合元素。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

4.哈希表對象

如果TYPE的值為REDIS_RDB_TYPE_HASH,那麼value儲存的就是一個REDIS_ENCODING_HT編碼的集合對象,RDB檔案儲存這種對象的結構:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
  • hash_size記錄了哈希表的大小,也即是這個哈希表儲存了多少鍵值對,讀入程式可以通過這個大小知道自己應該讀入多少個鍵值對。
  • 以key_value_pair開頭的部分代表哈希表中的鍵值對,鍵值對的鍵和值都是字元串對象,是以程式會以處理字元串對象的方式來儲存和讀入鍵值對。

5.有序集合對象

如果TYPE的值為REDIS_RDB_TYPE_ZSET,那麼value儲存的就是一個REDIS_ENCODING_SKIPLIST編碼的有序集合對象,RDB檔案儲存這種對象的結構:

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

sorted_set_size記錄了有序集合的大小,也即是這個有序集合儲存了多少元素,讀入程式需要根據這個值來決定應該讀入多少有序集合元素。

以element開頭的部分代表有序集合中的元素,每個元素又分為成員(member)和分值(score)兩部分,成員是一個字元串對象,分值則是一個double類型的浮點數,程式在儲存RDB檔案時會先将分值轉換成字元串對象,然後再用儲存字元串對象的方法将分值儲存起來。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

6.INTSET編碼的集合

如果TYPE的值為REDIS_RDB_TYPE_SET_INTSET,那麼value儲存的就是一個整數集合對象,RDB檔案儲存這種對象的方法是,先将整數集合轉換為字元串對象,然後将這個字元串對象儲存到RDB檔案裡面。

如果程式在讀入RDB檔案的過程中,碰到由整數集合對象轉換成的字元串對象,那麼程式會根據TYPE值的訓示,先讀入字元串對象,再将這個字元串對象轉換成原來的整數集合對象。

7.ZIPLIST編碼的清單、哈希表或者有序集合

如果TYPE的值為REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或者REDIS_RDB_TYPE_ZSET_ZIPLIST,那麼value儲存的就是一個壓縮清單對象,RDB檔案儲存這種對象的方法是:

  • 1)将壓縮清單轉換成一個字元串對象。
  • 2)将轉換所得的字元串對象儲存到RDB檔案。

如果程式在讀入RDB檔案的過程中,碰到由壓縮清單對象轉換成的字元串對象,那麼程式會根據TYPE值的訓示,執行以下操作:

  • 1)讀入字元串對象,并将它轉換成原來的壓縮清單對象。
  • 2)根據TYPE的值,設定壓縮清單對象的類型:如果TYPE的值為REDIS_RDB_TYPE_LIST_ZIPLIST,那麼壓縮清單對象的類型為清單;如果TYPE的值為REDIS_RDB_TYPE_HASH_ZIPLIST,那麼壓縮清單對象的類型為哈希表;如果TYPE的值為REDIS_RDB_TYPE_ZSET_ZIPLIST,那麼壓縮清單對象的類型為有序集合。

由于TYPE的存在,即使清單、哈希表和有序集合三種類型都使用壓縮清單來儲存,RDB讀入程式也總可以将讀入并轉換之後得出的壓縮清單設定成原來的類型。

4、重點回顧

  • RDB檔案用于儲存和還原Redis伺服器所有資料庫中的所有鍵值對資料。
  • SAVE指令由伺服器程序直接執行儲存操作,是以該指令會阻塞伺服器。
  • BGSAVE令由子程序執行儲存操作,是以該指令不會阻塞伺服器。
  • 伺服器狀态中會儲存所有用save選項設定的儲存條件,當任意一個儲存條件被滿足時,伺服器會自動執行BGSAVE指令。
  • RDB檔案是一個經過壓縮的二進制檔案,由多個部分組成。
  • 對于不同類型的鍵值對,RDB檔案會使用不同的方式來儲存它們。

二、AOF持久化

除了RDB持久化功能之外,Redis還提供了AOF(Append Only File)持久化功能。與RDB持久化通過儲存資料庫中的鍵值對來記錄資料庫狀态不同,AOF持久化是通過儲存Redis伺服器所執行的寫指令來記錄資料庫狀态的。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

如果我們對空白的資料庫執行以下寫指令,那麼資料庫中将包含三個鍵值對:

redis> SET msg "hello"
OK
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> RPUSH numbers 128 256 512
(integer) 3
           

RDB持久化儲存資料庫狀态的方法是将msg、fruits、numbers三個鍵的鍵值對儲存到RDB檔案中,而AOF持久化儲存資料庫狀态的方法則是将伺服器執行的SET、SADD、RPUSH三個指令儲存到AOF檔案中。

被寫入AOF檔案的所有指令都是以Redis的指令請求協定格式儲存的,因為Redis的指令請求協定是純文字格式,是以我們可以直接打開一個AOF檔案,觀察裡面的内容。

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n
           

在這個AOF檔案裡面,除了用于指定資料庫的SELECT指令是伺服器自動添加的之外,其他都是我們之前通過用戶端發送的指令。

伺服器在啟動時,可以通過載入和執行AOF檔案中儲存的指令來還原伺服器關閉之前的資料庫狀态,以下就是伺服器載入AOF檔案并還原資料庫狀态時列印的日志:

[8321] 05 Sep 11:58:50.448 # Server started, Redisversion 2.9.11
[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds
[8321] 05 Sep 11:58:50.449 * The server is now ready to accept connections on port 6379
           

1、AOF持久化的實作

AOF持久化功能的實作可以分為**指令追加(append)、檔案寫入、檔案同步(sync)**三個步驟。

1.1 指令追加

當AOF持久化功能處于打開狀态時,伺服器在執行完一個寫指令之後,會以協定格式将被執行的寫指令追加到伺服器狀态的aof_buf緩沖區的末尾:

struct redisServer {
    // ...
    // AOF緩沖區
    sds aof_buf;
    // ...
};
           

如果用戶端向伺服器發送以下指令:

redis> SET KEY VALUE
OK
           

那麼伺服器在執行這個SET指令之後,會将以下協定内容追加到aof_buf緩沖區的末尾:

*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
           

1.2 AOF檔案的寫入與同步

Redis的伺服器程序就是一個事件循環(loop),這個循環中的檔案事件負責接收用戶端的指令請求,以及向用戶端發送指令回複,而時間事件則負責執行像serverCron函數這樣需要定時運作的函數。

因為伺服器在處理檔案事件時可能會執行寫指令,使得一些内容被追加到aof_buf緩沖區裡面,是以在伺服器每次結束一個事件循環之前,它都會調用flushAppendOnlyFile函數,考慮是否需要将aof_buf緩沖區中的内容寫入和儲存到AOF檔案裡面。

def eventLoop():
    while True:
        # 處理檔案事件,接收指令請求以及發送指令回複
        # 處理指令請求時可能會有新内容被追加到 aof_buf 緩沖區中
        processFileEvents()
        # 處理時間事件
        processTimeEvents()
        # 考慮是否要将 aof_buf 中的内容寫入和儲存到 AOF 檔案裡面
        flushAppendOnlyFile()
           

flushAppendOnlyFile函數的行為由伺服器配置的appendfsync選項的值來決定,各個不同值産生的行為

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

如果使用者沒有主動為appendfsync選項設定值,那麼appendfsync選項的預設值為everysec,關于appendfsync選項的更多資訊,請參考Redis項目附帶的示例配置檔案redis.conf。

檔案的寫入和同步

為了提高檔案的寫入效率,在現代作業系統中,當使用者調用write函數,将一些資料寫入到檔案的時候,作業系統通常會将寫入資料暫時儲存在一個記憶體緩沖區裡面,等到緩沖區的空間被填滿、或者超過了指定的時限之後,才真正地将緩沖區中的資料寫入到磁盤裡面。

這種做法雖然提高了效率,但也為寫入資料帶來了安全問題,因為如果計算機發生停機,那麼儲存在記憶體緩沖區裡面的寫入資料将會丢失。為此,系統提供了fsync和fdatasync兩個同步函數,它們可以強制讓作業系統立即将緩沖區中的資料寫入到硬碟裡面,進而確定寫入資料的安全性。

假設伺服器在處理檔案事件期間,執行了以下三個寫入指令:

1) SADD databases "Redis" "MongoDB" "MariaDB" 
2) SET date "2013-9-5" 
3) INCR click_counter 10086
           

那麼aof_buf緩沖區将包含這三個指令的協定内容:

*5\r\n$4\r\nSADD\r\n$9\r\ndatabases\r\n$5\r\nRedis\r\n$7\r\nMongoDB\r\n$7\r\nMariaDB\r\n
*3\r\n$3\r\nSET\r\n$4\r\ndate\r\n$8\r\n2013-9-5\r\n
*3\r\n$4\r\nINCR\r\n$13\r\nclick_counter\r\n$5\r\n10086\r\n
           

如果這時flushAppendOnlyFile函數被調用,假設伺服器目前appendfsync選項的值為everysec,并且距離上次同步AOF檔案已經超過一秒鐘,那麼伺服器會先将aof_buf中的内容寫入到AOF檔案中,然後再對AOF檔案進行同步。

伺服器配置appendfsync選項的值直接決定AOF持久化功能的效率和安全性。

  • 當appendfsync的值為always時,伺服器在每個事件循環都要将aof_buf緩沖區中的所有内容寫入到AOF檔案,并且同步AOF檔案,是以always的效率是appendfsync選項三個值當中最慢的一個,但從安全性來說,always也是最安全的,因為即使出現故障停機,AOF持久化也隻會丢失一個事件循環中所産生的指令資料。
  • 當appendfsync的值為everysec時,伺服器在每個事件循環都要将aof_buf緩沖區中的所有内容寫入到AOF檔案,并且每隔一秒就要在子線程中對AOF檔案進行一次同步。從效率上來講,everysec模式足夠快,并且就算出現故障停機,資料庫也隻丢失一秒鐘的指令資料。
  • 當appendfsync的值為no時,伺服器在每個事件循環都要将aof_buf緩沖區中的所有内容寫入到AOF檔案,至于何時對AOF檔案進行同步,則由作業系統控制。因為處于no模式下的flushAppendOnlyFile調用無須執行同步操作,是以該模式下的AOF檔案寫入速度總是最快的,不過因為這種模式會在系統緩存中積累一段時間的寫入資料,是以該模式的單次同步時長通常是三種模式中時間最長的。從平攤操作的角度來看,no模式和everysec模式的效率類似,當出現故障停機時,使用no模式的伺服器将丢失上次同步AOF檔案之後的所有寫指令資料。

2、AOF檔案的載入與資料還原

AOF檔案裡面包含了重建資料庫狀态所需的所有寫指令,是以伺服器隻要讀入并重新執行一遍AOF檔案裡面儲存的寫指令,就可以還原伺服器關閉之前的資料庫狀态。

Redis讀取AOF檔案并還原資料庫狀态的詳細步驟如下:

  • 1)建立一個不帶網絡連接配接的僞用戶端(fake client):因為Redis的指令隻能在用戶端上下文中執行,而載入AOF檔案時所使用的指令直接來源于AOF檔案而不是網絡連接配接,是以伺服器使用了一個沒有網絡連接配接的僞用戶端來執行AOF檔案儲存的寫指令,僞用戶端執行指令的效果和帶網絡連接配接的用戶端執行指令的效果完全一樣。
  • 2)從AOF檔案中分析并讀取出一條寫指令。
  • 3)使用僞用戶端執行被讀出的寫指令。
  • 4)一直執行步驟2和步驟3,直到AOF檔案中的所有寫指令都被處理完畢為止。
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n
           

伺服器首先讀入并執行SELECT 0指令,之後是SET msg hello指令,再之後是SADD fruits apple banana cherry指令,最後是RPUSH numbers 128 256 512指令,當這些指令都執行完畢之後,伺服器的資料庫就被還原到之前的狀态了。

3、AOF重寫

因為AOF持久化是通過儲存被執行的寫指令來記錄資料庫狀态的,是以随着伺服器運作時間的流逝,AOF檔案中的内容會越來越多,檔案的體積也會越來越大,如果不加以控制的話,體積過大的AOF檔案很可能對Redis伺服器、甚至整個宿主計算機造成影響,并且AOF檔案的體積越大,使用AOF檔案來進行資料還原所需的時間就越多。

redis> RPUSH list "A" "B"            // ["A", "B"]
(integer) 2
redis> RPUSH list "C"                // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E"            // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list                     // ["B", "C", "D", "E"]
"A"
redis> LPOP list                     // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G"            // ["C", "D", "E", "F", "G"]
(integer) 5
           

那麼光是為了記錄這個list鍵的狀态,AOF檔案就需要儲存六條指令。

為了解決AOF檔案體積膨脹的問題,Redis提供了AOF檔案重寫(rewrite)功能。通過該功能,Redis伺服器可以建立一個新的AOF檔案來替代現有的AOF檔案,新舊兩個AOF檔案所儲存的資料庫狀态相同,但新AOF檔案不會包含任何浪費空間的備援指令,是以新AOF檔案的體積通常會比舊AOF檔案的體積要小得多。

3.1 AOF檔案重寫的實作

雖然Redis将生成新AOF檔案替換舊AOF檔案的功能命名為“AOF檔案重寫”,但實際上,AOF檔案重寫并不需要對現有的AOF檔案進行任何讀取、分析或者寫入操作,這個功能是通過讀取伺服器目前的資料庫狀态來實作的。

redis> RPUSH list "A" "B"            // ["A", "B"]
(integer) 2
redis> RPUSH list "C"                // ["A", "B", "C"]
(integer) 3
redis> RPUSH list "D" "E"            // ["A", "B", "C", "D", "E"]
(integer) 5
redis> LPOP list                     // ["B", "C", "D", "E"]
"A"
redis> LPOP list                     // ["C", "D", "E"]
"B"
redis> RPUSH list "F" "G"            // ["C", "D", "E", "F", "G"]
(integer) 5
           

那麼伺服器為了儲存目前list鍵的狀态,必須在AOF檔案中寫入六條指令。

如果伺服器想要用盡量少的指令來記錄list鍵的狀态,那麼最簡單高效的辦法不是去讀取和分析現有AOF檔案的内容,而是直接從資料庫中讀取鍵list的值,然後用一條RPUSH list"C"“D”“E”“F”"G"指令來代替儲存在AOF檔案中的六條指令,這樣就可以将儲存list鍵所需的指令從六條減少為一條了。

除了上面列舉的清單鍵之外,其他所有類型的鍵都可以用同樣的方法去減少AOF檔案中的指令數量。首先從資料庫中讀取鍵現在的值,然後用一條指令去記錄鍵值對,代替之前記錄這個鍵值對的多條指令,這就是AOF重寫功能的實作原理。

因為aof_rewrite函數生成的新AOF檔案隻包含還原目前資料庫狀态所必須的指令,是以新AOF檔案不會浪費任何硬碟空間。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

aof_rewrite函數産生的新AOF檔案将包含以下指令:

SELECT 0
RPUSH alphabet "a" "b" "c"
EXPIREAT alphabet 1385877600000
HMSET book "name" "Redisin Action"
           "author" "Josiah L. Carlson"
           "publisher" "Manning"
EXPIREAT book 1388556000000
SET message "hello world"
           

以上指令就是還原資料庫所必須的指令,它們沒有一條是多餘的。

在實際中,為了避免在執行指令時造成用戶端輸入緩沖區溢出,重寫程式在處理清單、哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數量,如果元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那麼重寫程式将使用多條指令來記錄鍵的值,而不單單使用一條指令。

3.2 AOF背景重寫

AOF重寫程式aof_rewrite函數可以很好地完成建立一個新AOF檔案的任務,但是,因為這個函數會進行大量的寫入操作,是以調用這個函數的線程将被長時間阻塞,因為Redis伺服器使用單個線程來處理指令請求,是以如果由伺服器直接調用aof_rewrite函數的話,那麼在重寫AOF檔案期間,服務期将無法處理用戶端發來的指令請求。

很明顯,作為一種輔佐性的維護手段,Redis不希望AOF重寫造成伺服器無法處理請求,是以Redis決定将AOF重寫程式放到子程序裡執行,這樣做可以同時達到兩個目的:

  • 子程序進行AOF重寫期間,伺服器程序(父程序)可以繼續處理指令請求。
  • 子程序帶有伺服器程序的資料副本,使用子程序而不是線程,可以在避免使用鎖的情況下,保證資料的安全性。

因為子程序在進行AOF重寫期間,伺服器程序還需要繼續處理指令請求,而新的指令可能會對現有的資料庫狀态進行修改,進而使得伺服器目前的資料庫狀态和重寫後的AOF檔案所儲存的資料庫狀态不一緻。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

為了解決這種資料不一緻問題,Redis伺服器設定了一個AOF重寫緩沖區,這個緩沖區在伺服器建立子程序之後開始使用,當Redis伺服器執行完一個寫指令之後,它會同時将這個寫指令發送給AOF緩沖區和AOF重寫緩沖區。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

當子程序完成AOF重寫工作之後,它會向父程序發送一個信号,父程序在接到該信号之後,會調用一個信号處理函數,并執行以下工作:

  • 1)将AOF重寫緩沖區中的所有内容寫入到新AOF檔案中,這時新AOF檔案所儲存的資料庫狀态将和伺服器目前的資料庫狀态一緻。
  • 2)對新的AOF檔案進行改名,原子地(atomic)覆寫現有的AOF檔案,完成新舊兩個AOF檔案的替換。

這個信号處理函數執行完畢之後,父程序就可以繼續像往常一樣接受指令請求了。

在整個AOF背景重寫過程中,隻有信号處理函數執行時會對伺服器程序(父程序)造成阻塞,在其他時候,AOF背景重寫都不會阻塞父程序,這将AOF重寫對伺服器性能造成的影響降到了最低。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

4、重點回顧

  • AOF檔案通過儲存所有修改資料庫的寫指令請求來記錄伺服器的資料庫狀态。
  • AOF檔案中的所有指令都以Redis指令請求協定的格式儲存。
  • 指令請求會先儲存到AOF緩沖區裡面,之後再定期寫入并同步到AOF檔案。
  • appendfsync選項的不同值對AOF持久化功能的安全性以及Redis伺服器的性能有很大的影響。
  • 伺服器隻要載入并重新執行儲存在AOF檔案中的指令,就可以還原資料庫本來的狀态。
  • AOF重寫可以産生一個新的AOF檔案,這個新的AOF檔案和原有的AOF檔案所儲存的資料庫狀态一樣,但體積更小。
  • AOF重寫是一個有歧義的名字,該功能是通過讀取資料庫中的鍵值對來實作的,程式無須對現有AOF檔案進行任何讀入、分析或者寫入操作。
  • 在執行BGREWRITEAOF指令時,Redis伺服器會維護一個AOF重寫緩沖區,該緩沖區會在子程序建立新AOF檔案期間,記錄伺服器執行的所有寫指令。當子程序完成建立新AOF檔案的工作之後,伺服器會将重寫緩沖區中的所有内容追加到新AOF檔案的末尾,使得新舊兩個AOF檔案所儲存的資料庫狀态一緻。最後,伺服器用新的AOF檔案替換舊的AOF檔案,以此來完成AOF檔案重寫操作。

三、事件

Redis伺服器是一個事件驅動程式,伺服器需要處理以下兩類事件:

  • 檔案事件(file event):Redis伺服器通過套接字與用戶端(或者其他Redis伺服器)進行連接配接,而檔案事件就是伺服器對套接字操作的抽象。伺服器與用戶端(或者其他伺服器)的通信會産生相應的檔案事件,而伺服器則通過監聽并處理這些事件來完成一系列網絡通信操作。
  • 時間事件(time event):Redis伺服器中的一些操作(比如serverCron函數)需要在給定的時間點執行,而時間事件就是伺服器對這類定時操作的抽象。

1、檔案事件

Redis基于Reactor模式開發了自己的網絡事件處理器:這個處理器被稱為檔案事件處理器(file event handler):

  • 檔案事件處理器使用I/O多路複用(multiplexing)程式來同時監聽多個套接字,并根據套接字目前執行的任務來為套接字關聯不同的事件處理器。
  • 當被監聽的套接字準備好執行連接配接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案事件就會産生,這時檔案事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

雖然檔案事件處理器以單線程方式運作,但通過使用I/O多路複用程式來監聽多個套接字,檔案事件處理器既實作了高性能的網絡通信模型,又可以很好地與Redis伺服器中其他同樣以單線程方式運作的子產品進行對接,這保持了Redis内部單線程設計的簡單性。

1.1 檔案事件處理器的構成

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

檔案事件是對套接字操作的抽象,每當一個套接字準備好執行連接配接應答(accept)、寫入、讀取、關閉等操作時,就會産生一個檔案事件。因為一個伺服器通常會連接配接多個套接字,是以多個檔案事件有可能會并發地出現。

I/O多路複用程式負責監聽多個套接字,并向檔案事件分派器傳送那些産生了事件的套接字。

盡管多個檔案事件可能會并發地出現,但I/O多路複用程式總是會将所有産生事件的套接字都放到一個隊列裡面,然後通過這個隊列,以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向檔案事件分派器傳送套接字。當上一個套接字産生的事件被處理完畢之後(該套接字為事件所關聯的事件處理器執行完畢),I/O多路複用程式才會繼續向檔案事件分派器傳送下一個套接字。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

檔案事件分派器接收I/O多路複用程式傳來的套接字,并根據套接字産生的事件的類型,調用相應的事件處理器。

伺服器會為執行不同任務的套接字關聯不同的事件處理器,這些處理器是一個個函數,它們定義了某個事件發生時,伺服器應該執行的動作。

1.2 I/O多路複用程式的實作

Redis的I/O多路複用程式的所有功能都是通過包裝常見的select、epoll、evport和kqueue這些I/O多路複用函數庫來實作的,每個I/O多路複用函數庫在Redis源碼中都對應一個單獨的檔案,比如ae_select.c、ae_epoll.c、ae_kqueue.c,諸如此類。

因為Redis為每個I/O多路複用函數庫都實作了相同的API,是以I/O多路複用程式的底層實作是可以互換的。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

1.3 事件的類型

I/O多路複用程式可以監聽多個套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和套接字操作之間的對應關系如下:

  • 當套接字變得可讀時(用戶端對套接字執行write操作,或者執行close操作),或者有新的可應答(acceptable)套接字出現時(用戶端對伺服器的監聽套接字執行connect操作),套接字産生AE_READABLE事件。
  • 當套接字變得可寫時(用戶端對套接字執行read操作),套接字産生AE_WRITABLE事件。

I/O多路複用程式允許伺服器同時監聽套接字的AE_READABLE事件和AE_WRITABLE事件,如果一個套接字同時産生了這兩種事件,那麼檔案事件分派器會優先處理AE_READABLE事件,等到AE_READABLE事件處理完之後,才處理AE_WRITABLE事件。

1.4 檔案事件的處理器

Redis為檔案事件編寫了多個處理器,這些事件處理器分别用于實作不同的網絡通信需求,比如說:

  • 為了對連接配接伺服器的各個用戶端進行應答,伺服器要為監聽套接字關聯連接配接應答處理器。
  • 為了接收用戶端傳來的指令請求,伺服器要為用戶端套接字關聯指令請求處理器。
  • 為了向用戶端傳回指令的執行結果,伺服器要為用戶端套接字關聯指令回複處理器。
  • 當主伺服器和從伺服器進行複制操作時,主從伺服器都需要關聯特别為複制功能編寫的複制處理器。

1.連接配接應答處理器

當Redis伺服器進行初始化的時候,程式會将這個連接配接應答處理器和伺服器監聽套接字的AE_READABLE事件關聯起來,當有用戶端用sys/socket.h/connect函數連接配接伺服器監聽套接字的時候,套接字就會産生AE_READABLE事件,引發連接配接應答處理器執行,并執行相應的套接字應答操作。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

2.指令請求處理器

當一個用戶端通過連接配接應答處理器成功連接配接到伺服器之後,伺服器會将用戶端套接字的AE_READABLE事件和指令請求處理器關聯起來,當用戶端向伺服器發送指令請求的時候,套接字就會産生AE_READABLE事件,引發指令請求處理器執行,并執行相應的套接字讀入操作。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

在用戶端連接配接伺服器的整個過程中,伺服器都會一直為用戶端套接字的AE_READABLE事件關聯指令請求處理器。

3.指令回複處理器

當伺服器有指令回複需要傳送給用戶端的時候,伺服器會将用戶端套接字的AE_WRITABLE事件和指令回複處理器關聯起來,當用戶端準備好接收伺服器傳回的指令回複時,就會産生AE_WRITABLE事件,引發指令回複處理器執行,并執行相應的套接字寫入操作。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

4.一次完整的用戶端與伺服器連接配接事件示例

  1. 假設一個Redis伺服器正在運作,那麼這個伺服器的監聽套接字的AE_READABLE事件應該正處于監聽狀态之下,而該事件所對應的處理器為連接配接應答處理器。
  2. 如果這時有一個Redis用戶端向伺服器發起連接配接,那麼監聽套接字将産生AE_READABLE事件,觸發連接配接應答處理器執行。處理器會對用戶端的連接配接請求進行應答,然後建立用戶端套接字,以及用戶端狀态,并将用戶端套接字的AE_READABLE事件與指令請求處理器進行關聯,使得用戶端可以向主伺服器發送指令請求。
  3. 之後,假設用戶端向主伺服器發送一個指令請求,那麼用戶端套接字将産生AE_READABLE事件,引發指令請求處理器執行,處理器讀取用戶端的指令内容,然後傳給相關程式去執行。
  4. 執行指令将産生相應的指令回複,為了将這些指令回複傳送回用戶端,伺服器會将用戶端套接字的AE_WRITABLE事件與指令回複處理器進行關聯。當用戶端嘗試讀取指令回複的時候,用戶端套接字将産生AE_WRITABLE事件,觸發指令回複處理器執行,當指令回複處理器将指令回複全部寫入到套接字之後,伺服器就會解除用戶端套接字的AE_WRITABLE事件與指令回複處理器之間的關聯。
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

2、時間事件

Redis的時間事件分為以下兩類:

  • 定時事件:讓一段程式在指定的時間之後執行一次。比如說,讓程式X在目前時間的30毫秒之後執行一次。
  • 周期性事件:讓一段程式每隔指定時間就執行一次。比如說,讓程式Y每隔30毫秒就執行一次。

一個時間事件主要由以下三個屬性組成:

  • id:伺服器為時間事件建立的全局唯一ID(辨別号)。ID号按從小到大的順序遞增,新事件的ID号比舊事件的ID号要大。
  • when:毫秒精度的UNIX時間戳,記錄了時間事件的到達(arrive)時間。
  • timeProc:時間事件處理器,一個函數。當時間事件到達時,伺服器就會調用相應的處理器來處理事件。

一個時間事件是定時事件還是周期性事件取決于時間事件處理器的傳回值:

  • 如果事件處理器傳回ae.h/AE_NOMORE,那麼這個事件為定時事件:該事件在達到一次之後就會被删除,之後不再到達。
  • 如果事件處理器傳回一個非AE_NOMORE的整數值,那麼這個事件為周期性時間:當一個時間事件到達之後,伺服器會根據事件處理器傳回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之後再次到達,并以這種方式一直更新并運作下去。比如說,如果一個時間事件的處理器傳回整數值30,那麼伺服器應該對這個時間事件進行更新,讓這個事件在30毫秒之後再次到達。

2.1 實作

伺服器将所有時間事件都放在一個無序連結清單中,每當時間事件執行器運作時,它就周遊整個連結清單,查找所有已到達的時間事件,并調用相應的事件處理器。

Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
注意,我們說儲存時間事件的連結清單為無序連結清單,指的不是連結清單不按ID排序,而是說,該連結清單不按when屬性的大小排序。正因為連結清單沒有按when屬性進行排序,是以當時間事件執行器運作的時候,它必須周遊連結清單中的所有時間事件,這樣才能確定伺服器中所有已到達的時間事件都會被處理。

無序連結清單并不影響時間事件處理器的性能

在目前版本中,正常模式下的Redis伺服器隻使用serverCron一個時間事件,而在benchmark模式下,伺服器也隻使用兩個時間事件。在這種情況下,伺服器幾乎是将無序連結清單退化成一個指針來使用,是以使用無序連結清單來儲存時間事件,并不影響事件執行的性能。

def processTimeEvents():
    # 周遊伺服器中的所有時間事件
    for time_event in all_time_event():
        # 檢查事件是否已經到達
        if time_event.when <= unix_ts_now():
            # 事件已到達
            # 執行事件處理器,并擷取傳回值
            retval = time_event.timeProc()
            # 如果這是一個定時事件
            if retval == AE_NOMORE:
                # 那麼将該事件從伺服器中删除
                delete_time_event_from_server(time_event)
        # 如果這是一個周期性事件
        else:
            # 那麼按照事件處理器的傳回值更新時間事件的 when 屬性
            # 讓這個事件在指定的時間之後再次到達
            update_when(time_event, retval)
           

2.2 時間事件應用執行個體:serverCron函數

持續運作的Redis伺服器需要定期對自身的資源和狀态進行檢查和調整,進而確定伺服器可以長期、穩定地運作,這些定期操作由redis.c/serverCron函數負責執行,它的主要工作包括:

  • 更新伺服器的各類統計資訊,比如時間、記憶體占用、資料庫占用情況等。
  • 清理資料庫中的過期鍵值對。
  • 關閉和清理連接配接失效的用戶端。
  • 嘗試進行AOF或RDB持久化操作。
  • 如果伺服器是主伺服器,那麼對從伺服器進行定期同步。
  • 如果處于叢集模式,對叢集進行定期同步和連接配接測試。

Redis伺服器以周期性事件的方式來運作serverCron函數,在伺服器運作期間,每隔一段時間,serverCron就會執行一次,直到伺服器關閉為止。

在Redis2.6版本,伺服器預設規定serverCron每秒運作10次,平均每間隔100毫秒運作一次。

3、事件的排程與執行

因為伺服器中同時存在檔案事件和時間事件兩種事件類型,是以伺服器必須對這兩種事件進行排程,決定何時應該處理檔案事件,何時又應該處理時間事件,以及花多少時間來處理它們等等。

事件的排程和執行由ae.c/aeProcessEvents函數負責

def aeProcessEvents():
    # 擷取到達時間離目前時間最接近的時間事件
    time_event = aeSearchNearestTimer()
    # 計算最接近的時間事件距離到達還有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
    # 如果事件已到達,那麼remaind_ms的值可能為負數,将它設定為0
    if remaind_ms < 0:
        remaind_ms = 0
    # 根據remaind_ms的值,建立timeval結構
    timeval = create_timeval_with_ms(remaind_ms)
    # 阻塞并等待檔案事件産生,最大阻塞時間由傳入的timeval結構決定
    # 如果remaind_ms的值為0,那麼aeApiPoll調用之後馬上傳回,不阻塞
    aeApiPoll(timeval)
    # 處理所有已産生的檔案事件
    processFileEvents()
    # 處理所有已到達的時間事件
    processTimeEvents()
           

将aeProcessEvents函數置于一個循環裡面,加上初始化和清理函數,這就構成了Redis伺服器的主函數

def main():
    # 初始化伺服器
    init_server()
    # 一直處理事件,直到伺服器關閉為止
    while server_is_not_shutdown():
        aeProcessEvents()
    # 伺服器關閉,執行清理操作
    clean_server()
           
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件

以下是事件的排程和執行規則:

  • 1)aeApiPoll函數的最大阻塞時間由到達時間最接近目前時間的時間事件決定,這個方法既可以避免伺服器對時間事件進行頻繁的輪詢(忙等待),也可以確定aeApiPoll函數不會阻塞過長時間。
  • 2)因為檔案事件是随機出現的,如果等待并處理完一次檔案事件之後,仍未有任何時間事件到達,那麼伺服器将再次等待并處理檔案事件。随着檔案事件的不斷執行,時間會逐漸向時間事件所設定的到達時間逼近,并最終來到到達時間,這時伺服器就可以開始處理到達的時間事件了。
  • 3)對檔案事件和時間事件的處理都是同步、有序、原子地執行的,伺服器不會中途中斷事件處理,也不會對事件進行搶占,是以,不管是檔案事件的處理器,還是時間事件的處理器,它們都會盡可地減少程式的阻塞時間,并在有需要時主動讓出執行權,進而降低造成事件饑餓的可能性。比如說,在指令回複處理器将一個指令回複寫入到用戶端套接字時,如果寫入位元組數超過了一個預設常量的話,指令回複處理器就會主動用break跳出寫入循環,将餘下的資料留到下次再寫;另外,時間事件也會将非常耗時的持久化操作放到子線程或者子程序執行。
  • 4)因為時間事件在檔案事件之後執行,并且事件之間不會出現搶占,是以時間事件的實際處理時間,通常會比時間事件設定的到達時間稍晚一些。
Redis設計與實作讀書筆記三、Redis的持久化機制一、RDB持久化二、AOF持久化三、事件
  • 因為時間事件尚未到達,是以在處理時間事件之前,伺服器已經等待并處理了兩次檔案事件。
  • 因為處理事件的過程中不會出現搶占,是以實際處理時間事件的時間比預定的100毫秒慢了30毫秒。

4、重點回顧

  • Redis伺服器是一個事件驅動程式,伺服器處理的事件分為時間事件和檔案事件兩類。
  • 檔案事件處理器是基于Reactor模式實作的網絡通信程式。
  • 檔案事件是對套接字操作的抽象:每次套接字變為可應答(acceptable)、可寫(writable)或者可讀(readable)時,相應的檔案事件就會産生。
  • 檔案事件分為AE_READABLE事件(讀事件)和AE_WRITABLE事件(寫事件)兩類。
  • 時間事件分為定時事件和周期性事件:定時事件隻在指定的時間到達一次,而周期性事件則每隔一段時間到達一次。
  • 伺服器在一般情況下隻執行serverCron函數一個時間事件,并且這個事件是周期性事件。
  • 檔案事件和時間事件之間是合作關系,伺服器會輪流處理這兩種事件,并且處理事件的過程中也不會進行搶占。
  • 時間事件的實際處理時間通常會比設定的到達時間晚一些。

繼續閱讀