1. 程式人生 > >Redis AOF原理

Redis AOF原理

AOF 寫入鍵值對過程

Redis 分別提供了 RDB 和 AOF 兩種持久化機制:

  • RDB 將資料庫的快照(snapshot)以二進位制的方式儲存到磁碟中。
  • AOF 則以協議文字的方式,將所有對資料庫進行過寫入的命令(及其引數)記錄到 AOF 檔案,以此達到記錄資料庫狀態的目的。

digraph persistent {      rankdir = LR;      node [shape = circle, style = filled];      edge [style = "dashed, bold"];      // node      client [label = "客戶端", fillcolor = "#FADCAD"];      server [label = "伺服器", fillcolor = "#A8E270"];      aof [label = "AOF \n 檔案", fillcolor = "#95BBE3"];      // edge      client -> server [label = "命令請求"];     server -> aof [ label = "網路協議格式的\n命令內容"]; }

本章首先介紹 AOF 功能的運作機制, 瞭解命令是如何被儲存到 AOF 檔案裡的, 觀察不同的 AOF 儲存模式對資料的安全性、以及 Redis 效能的影響。

之後會介紹從 AOF 檔案中恢復資料庫狀態的方法,以及該方法背後的實現機制。

最後還會介紹對 AOF 進行重寫以調整檔案體積的方法, 並研究這種方法是如何在不改變資料庫狀態的前提下進行的。

因為本章涉及 AOF 執行的相關機制, 如果還沒了解過 AOF 功能的話, 請先閱讀 Redis 持久化手冊中關於 AOF 的部分 。

AOF 命令同步

Redis 將所有對資料庫進行過寫入的命令(及其引數)記錄到 AOF 檔案, 以此達到記錄資料庫狀態的目的, 為了方便起見, 我們稱呼這種記錄過程為同步。

舉個例子, 如果執行以下命令:

redis> RPUSH list 1 2 3 4
(integer) 4

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"

redis> KEYS *
1) "list"

redis> RPOP list
"4"

redis> LPOP list
"1"

redis> LPUSH list 1
(integer) 3

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"

那麼其中四條對資料庫有修改的寫入命令就會被同步到 AOF 檔案中:

RPUSH list 1 2 3 4

RPOP list

LPOP list

LPUSH list 1

為了處理的方便, AOF 檔案使用網路通訊協議的格式來儲存這些命令。

比如說, 上面列舉的四個命令在 AOF 檔案中就實際儲存如下:

*2
$6
SELECT
$1
0
*6
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
*2
$4
RPOP
$4
list
*2
$4
LPOP
$4
list
*3
$5
LPUSH
$4
list
$1
1

除了 SELECT 命令是 AOF 程式自己加上去的之外, 其他命令都是之前我們在終端裡執行的命令。

同步命令到 AOF 檔案的整個過程可以分為三個階段:

  1. 命令傳播:Redis 將執行完的命令、命令的引數、命令的引數個數等資訊傳送到 AOF 程式中。
  2. 快取追加:AOF 程式根據接收到的命令資料,將命令轉換為網路通訊協議的格式,然後將協議內容追加到伺服器的 AOF 快取中。
  3. 檔案寫入和儲存:AOF 快取中的內容被寫入到 AOF 檔案末尾,如果設定的 AOF 儲存條件被滿足的話, fsync 函式或者 fdatasync 函式會被呼叫,將寫入的內容真正地儲存到磁碟中。

以下幾個小節將詳細地介紹這三個步驟。

命令傳播

當一個 Redis 客戶端需要執行命令時, 它通過網路連線, 將協議文字傳送給 Redis 伺服器。

比如說, 要執行命令 SET KEY VALUE , 客戶端將向伺服器傳送文字 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。

伺服器在接到客戶端的請求之後, 它會根據協議文字的內容, 選擇適當的命令函式, 並將各個引數從字串文字轉換為 Redis 字串物件(StringObject)。

比如說, 針對上面的 SET 命令例子, Redis 將客戶端的命令指標指向實現 SET 命令的 setCommand 函式, 並建立三個 Redis 字串物件, 分別儲存 SET 、 KEY 和 VALUE 三個引數(命令也算作引數)。

每當命令函式成功執行之後, 命令引數都會被傳播到 AOF 程式, 以及 REPLICATION 程式(本節不討論這個,列在這裡只是為了完整性的考慮)。

這個執行並傳播命令的過程可以用以下偽程式碼表示:

if (execRedisCommand(cmd, argv, argc) == EXEC_SUCCESS):

    if aof_is_turn_on():
        # 傳播命令到 AOF 程式
        propagate_aof(cmd, argv, argc)

    if replication_is_turn_on():
        # 傳播命令到 REPLICATION 程式
        propagate_replication(cmd, argv, argc)

以下是該過程的流程圖:

digraph propagate {      node [shape = plaintext, style = filled];      edge [style = bold];      // node       exec [label = "命令執行成功", fillcolor = "#FADCAD"];      aof_choice [label = "AOF\n 功能已開啟?", shape = diamond, fillcolor = "#95BBE3"];      propagate_aof [label = "傳播命令到 AOF 程式", fillcolor = "#A8E270"];      replication_choice [label = "REPLICATION\n 功能已開啟?", shape = diamond, fillcolor = "#95BBE3"];      propagate_replication [label = "傳播命令到 REPLICATION 程式", fillcolor = "#A8E270"];      remaind_jobs [label = "處理後續步驟:\n清理資源、\n等等", fillcolor = "#FADCAD"];      // edge      exec -> aof_choice;      aof_choice -> propagate_aof [label = "是"];      propagate_aof -> replication_choice;      aof_choice -> replication_choice [label = "否"];      replication_choice -> remaind_jobs [label = "否"];      replication_choice -> propagate_replication [label = "是"];      propagate_replication -> remaind_jobs; }

快取追加

當命令被傳播到 AOF 程式之後, 程式會根據命令以及命令的引數, 將命令從字串物件轉換回原來的協議文字。

比如說, 如果 AOF 程式接受到的三個引數分別儲存著 SET 、 KEY 和 VALUE 三個字串, 那麼它將生成協議文字"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。

協議文字生成之後, 它會被追加到 redis.h/redisServer 結構的 aof_buf 末尾。

redisServer 結構維持著 Redis 伺服器的狀態, aof_buf 域則儲存著所有等待寫入到 AOF 檔案的協議文字:

struct redisServer {

    // 其他域...

    sds aof_buf;

    // 其他域...
};

至此, 追加命令到快取的步驟執行完畢。

綜合起來,整個快取追加過程可以分為以下三步:

  1. 接受命令、命令的引數、以及引數的個數、所使用的資料庫等資訊。
  2. 將命令還原成 Redis 網路通訊協議。
  3. 將協議文字追加到 aof_buf 末尾。

檔案寫入和儲存

每當伺服器常規任務函式被執行、 或者事件處理器被執行時, aof.c/flushAppendOnlyFile 函式都會被呼叫, 這個函式執行以下兩個工作:

WRITE:根據條件,將 aof_buf 中的快取寫入到 AOF 檔案。

SAVE:根據條件,呼叫 fsync 或 fdatasync 函式,將 AOF 檔案儲存到磁碟中。

兩個步驟都需要根據一定的條件來執行, 而這些條件由 AOF 所使用的儲存模式來決定, 以下小節就來介紹 AOF 所使用的三種儲存模式, 以及在這些模式下, 步驟 WRITE 和 SAVE 的呼叫條件。

AOF 儲存模式

Redis 目前支援三種 AOF 儲存模式,它們分別是:

  1. AOF_FSYNC_NO :不儲存。
  2. AOF_FSYNC_EVERYSEC :每一秒鐘儲存一次。
  3. AOF_FSYNC_ALWAYS :每執行一個命令儲存一次。

以下三個小節將分別討論這三種儲存模式。

不儲存

在這種模式下, 每次呼叫 flushAppendOnlyFile 函式, WRITE 都會被執行, 但 SAVE 會被略過。

在這種模式下, SAVE 只會在以下任意一種情況中被執行:

  • Redis 被關閉
  • AOF 功能被關閉
  • 系統的寫快取被重新整理(可能是快取已經被寫滿,或者定期儲存操作被執行)

這三種情況下的 SAVE 操作都會引起 Redis 主程序阻塞。

每一秒鐘儲存一次

在這種模式中, SAVE 原則上每隔一秒鐘就會執行一次, 因為 SAVE 操作是由後臺子執行緒呼叫的, 所以它不會引起伺服器主程序阻塞。

注意, 在上一句的說明裡面使用了詞語“原則上”, 在實際執行中, 程式在這種模式下對 fsync 或 fdatasync 的呼叫並不是每秒一次, 它和呼叫 flushAppendOnlyFile 函式時 Redis 所處的狀態有關。

每當 flushAppendOnlyFile 函式被呼叫時, 可能會出現以下四種情況:

  • 子執行緒正在執行 SAVE ,並且:

    1. 這個 SAVE 的執行時間未超過 2 秒,那麼程式直接返回,並不執行 WRITE 或新的 SAVE 。
    2. 這個 SAVE 已經執行超過 2 秒,那麼程式執行 WRITE ,但不執行新的 SAVE 。注意,因為這時 WRITE 的寫入必須等待子執行緒先完成(舊的) SAVE ,因此這裡 WRITE 會比平時阻塞更長時間。
  • 子執行緒沒有在執行 SAVE ,並且:

    1. 上次成功執行 SAVE 距今不超過 1 秒,那麼程式執行 WRITE ,但不執行 SAVE 。
    2. 上次成功執行 SAVE 距今已經超過 1 秒,那麼程式執行 WRITE 和 SAVE 。

可以用流程圖表示這四種情況:

digraph flush {      node [shape = plaintext, style = filled, fillcolor = "#FADCAD"];      edge [style = bold];      //      SAVE_running_choice [label = "SAVE 正在執行?", shape = diamond, fillcolor = "#A8E270"];      over_2_second_choice [label = "執行時間\n超過 2 秒?", shape = diamond, fillcolor = "#95BBE3"];      not_over_2_second [label = "情況 1 :\n函式直接返回\n 不執行 WRITE 或\n新的 SAVE"];      over_2_second [label = "情況 2 :\n執行 WRITE \n 但不執行新的 SAVE \n"];      SAVE_running_choice -> over_2_second_choice [label = "是"];      over_2_second_choice -> not_over_2_second [label = "否"];      over_2_second_choice -> over_2_second [label = "是"];      finish_over_2_second [label = "距離上次 SAVE\n 執行成功\n超過 1 秒?", shape = diamond, fillcolor = "#95BBE3"];      no [label = "情況 3 :\n 執行 WRITE \n 但不執行新的 SAVE "];      yes [label = "情況 4 :\n 執行 WRITE 和\n新的 SAVE\n"];      SAVE_running_choice -> finish_over_2_second [label = "否"];      finish_over_2_second -> yes [label = "是"];      finish_over_2_second -> no [label = "否"];  }

根據以上說明可以知道, 在“每一秒鐘儲存一次”模式下, 如果在情況 1 中發生故障停機, 那麼使用者最多損失小於 2 秒內所產生的所有資料。

如果在情況 2 中發生故障停機, 那麼使用者損失的資料是可以超過 2 秒的。

Redis 官網上所說的, AOF 在“每一秒鐘儲存一次”時發生故障, 只丟失 1 秒鐘資料的說法, 實際上並不準確。

每執行一個命令儲存一次

在這種模式下,每次執行完一個命令之後, WRITE 和 SAVE 都會被執行。

另外,因為 SAVE 是由 Redis 主程序執行的,所以在 SAVE 執行期間,主程序會被阻塞,不能接受命令請求。

AOF 儲存模式對效能和安全性的影響

在上一個小節, 我們簡短地描述了三種 AOF 儲存模式的工作方式, 現在, 是時候研究一下這三個模式在安全性和效能方面的區別了。

對於三種 AOF 儲存模式, 它們對伺服器主程序的阻塞情況如下:

  1. 不儲存(AOF_FSYNC_NO):寫入和儲存都由主程序執行,兩個操作都會阻塞主程序。
  2. 每一秒鐘儲存一次(AOF_FSYNC_EVERYSEC):寫入操作由主程序執行,阻塞主程序。儲存操作由子執行緒執行,不直接阻塞主程序,但儲存操作完成的快慢會影響寫入操作的阻塞時長。
  3. 每執行一個命令儲存一次(AOF_FSYNC_ALWAYS):和模式 1 一樣。

因為阻塞操作會讓 Redis 主程序無法持續處理請求, 所以一般說來, 阻塞操作執行得越少、完成得越快, Redis 的效能就越好。

模式 1 的儲存操作只會在AOF 關閉或 Redis 關閉時執行, 或者由作業系統觸發, 在一般情況下, 這種模式只需要為寫入阻塞, 因此它的寫入效能要比後面兩種模式要高, 當然, 這種效能的提高是以降低安全性為代價的: 在這種模式下, 如果執行的中途發生停機, 那麼丟失資料的數量由作業系統的快取沖洗策略決定。

模式 2 在效能方面要優於模式 3 , 並且在通常情況下, 這種模式最多丟失不多於 2 秒的資料, 所以它的安全性要高於模式 1 , 這是一種兼顧效能和安全性的儲存方案。

模式 3 的安全性是最高的, 但效能也是最差的, 因為伺服器必須阻塞直到命令資訊被寫入並儲存到磁碟之後, 才能繼續處理請求。

綜合起來,三種 AOF 模式的操作特性可以總結如下:

模式 WRITE 是否阻塞? SAVE 是否阻塞? 停機時丟失的資料量
AOF_FSYNC_NO 阻塞 阻塞 作業系統最後一次對 AOF 檔案觸發 SAVE 操作之後的資料。
AOF_FSYNC_EVERYSEC 阻塞 不阻塞 一般情況下不超過 2 秒鐘的資料。
AOF_FSYNC_ALWAYS 阻塞 阻塞 最多隻丟失一個命令的資料。

AOF 檔案的讀取和資料還原

AOF 檔案儲存了 Redis 的資料庫狀態, 而檔案裡面包含的都是符合 Redis 通訊協議格式的命令文字。

這也就是說, 只要根據 AOF 檔案裡的協議, 重新執行一遍裡面指示的所有命令, 就可以還原 Redis 的資料庫狀態了。

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

  1. 建立一個不帶網路連線的偽客戶端(fake client)。
  2. 讀取 AOF 所儲存的文字,並根據內容還原出命令、命令的引數以及命令的個數。
  3. 根據命令、命令的引數和命令的個數,使用偽客戶端執行該命令。
  4. 執行 2 和 3 ,直到 AOF 檔案中的所有命令執行完畢。

完成第 4 步之後, AOF 檔案所儲存的資料庫就會被完整地還原出來。

注意, 因為 Redis 的命令只能在客戶端的上下文中被執行, 而 AOF 還原時所使用的命令來自於 AOF 檔案, 而不是網路, 所以程式使用了一個沒有網路連線的偽客戶端來執行命令。 偽客戶端執行命令的效果, 和帶網路連線的客戶端執行命令的效果, 完全一樣。

整個讀取和還原過程可以用以下偽程式碼表示:

def READ_AND_LOAD_AOF():

    # 開啟並讀取 AOF 檔案
    file = open(aof_file_name)
    while file.is_not_reach_eof():

        # 讀入一條協議文字格式的 Redis 命令
        cmd_in_text = file.read_next_command_in_protocol_format()

        # 根據文字命令,查詢命令函式,並建立引數和引數個數等物件
        cmd, argv, argc = text_to_command(cmd_in_text)

        # 執行命令
        execRedisCommand(cmd, argv, argc)

    # 關閉檔案
    file.close()

作為例子, 以下是一個簡短的 AOF 檔案的內容:

*2
$6
SELECT
$1
0
*3
$3
SET
$3
key
$5
value
*8
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
$1
5
$1
6

當程式讀入這個 AOF 檔案時, 它首先執行 SELECT 0 命令 —— 這個 SELECT 命令是由 AOF 寫入程式自動生成的, 它確保程式可以將資料還原到正確的資料庫上。

然後執行後面的 SET key value 和 RPUSH 1 2 3 4 命令, 還原 key 和 list 兩個鍵的資料。

為了避免對資料的完整性產生影響, 在伺服器載入資料的過程中, 只有和資料庫無關的訂閱與釋出功能可以正常使用, 其他命令一律返回錯誤。

AOF 重寫

AOF 檔案通過同步 Redis 伺服器所執行的命令, 從而實現了資料庫狀態的記錄, 但是, 這種同步方式會造成一個問題: 隨著執行時間的流逝, 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]

那麼光是記錄 list 鍵的狀態, AOF 檔案就需要儲存四條命令。

另一方面, 有些被頻繁操作的鍵, 對它們所呼叫的命令可能有成百上千、甚至上萬條, 如果這樣被頻繁操作的鍵有很多的話, AOF 檔案的體積就會急速膨脹, 對 Redis 、甚至整個系統的造成影響。

為了解決以上的問題, Redis 需要對 AOF 檔案進行重寫(rewrite): 建立一個新的 AOF 檔案來代替原有的 AOF 檔案, 新 AOF 檔案和原有 AOF 檔案儲存的資料庫狀態完全一樣, 但新 AOF 檔案的體積小於等於原有 AOF 檔案的體積。

以下就來介紹 AOF 重寫的實現方式。

AOF 重寫的實現

所謂的“重寫”其實是一個有歧義的詞語, 實際上, AOF 重寫並不需要對原有的 AOF 檔案進行任何寫入和讀取, 它針對的是資料庫中鍵的當前值。

考慮這樣一個情況, 如果伺服器對鍵 list 執行了以下四條命令:

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]

那麼當前列表鍵 list 在資料庫中的值就為 [1, 2, 3] 。

如果我們要儲存這個列表的當前狀態, 並且儘量減少所使用的命令數, 那麼最簡單的方式不是去 AOF 檔案上分析前面執行的四條命令, 而是直接讀取 list 鍵在資料庫的當前值, 然後用一條 RPUSH 1 2 3 命令來代替前面的四條命令。

再考慮這樣一個例子, 如果伺服器對集合鍵 animal 執行了以下命令:

SADD animal cat                 // {cat}

SADD animal dog panda tiger     // {cat, dog, panda, tiger}

SREM animal cat                 // {dog, panda, tiger}

SADD animal cat lion            // {cat, lion, dog, panda, tiger}

那麼使用一條 SADD animal cat lion dog panda tiger 命令, 就可以還原 animal 集合的狀態, 這比之前的四條命令呼叫要大大減少。

除了列表和集合之外, 字串、有序集、雜湊表等鍵也可以用類似的方法來儲存狀態, 並且儲存這些狀態所使用的命令數量, 比起之前建立這些鍵的狀態所使用命令的數量要大大減少。

根據鍵的型別, 使用適當的寫入命令來重現鍵的當前值, 這就是 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()

AOF 後臺重寫

上一節展示的 AOF 重寫程式可以很好地完成建立一個新 AOF 檔案的任務, 但是, 在執行這個程式的時候, 呼叫者執行緒會被阻塞。

很明顯, 作為一種輔佐性的維護手段, Redis 不希望 AOF 重寫造成伺服器無法處理請求, 所以 Redis 決定將 AOF 重寫程式放到(後臺)子程序裡執行, 這樣處理的最大好處是:

  1. 子程序進行 AOF 重寫期間,主程序可以繼續處理命令請求。
  2. 子程序帶有主程序的資料副本,使用子程序而不是執行緒,可以在避免鎖的情況下,保證資料的安全性。

不過, 使用子程序也有一個問題需要解決: 因為子程序在進行 AOF 重寫期間, 主程序還需要繼續處理命令, 而新的命令可能對現有的資料進行修改, 這會讓當前資料庫的資料和重寫後的 AOF 檔案中的資料不一致。

為了解決這個問題, Redis 增加了一個 AOF 重寫快取, 這個快取在 fork 出子程序之後開始啟用, Redis 主程序在接到新的寫命令之後, 除了會將這個寫命令的協議內容追加到現有的 AOF 檔案之外, 還會追加到這個快取中:

digraph p {      node [style = filled];      edge [style = "bold, dashed"];      //       client [label = "客戶端", fillcolor = "#95BBE3"];      server [label = "伺服器", fillcolor = "#A8E270"];      client -> server [label = "命令請求"];      current_aof [label = "現有 AOF 檔案", shape = box, fillcolor = "#FADCAD"];      aof_rewrite_buf [label = "AOF 重寫快取", shape = box, fillcolor = "#FADCAD"];      server -> current_aof [label = "命令協議內容"];      server -> aof_rewrite_buf [label = "命令協議內容"]; }

換言之, 當子程序在執行 AOF 重寫時, 主程序需要執行以下三個工作:

  1. 處理命令請求。
  2. 將寫命令追加到現有的 AOF 檔案中。
  3. 將寫命令追加到 AOF 重寫快取中。

這樣一來可以保證:

  1. 現有的 AOF 功能會繼續執行,即使在 AOF 重寫期間發生停機,也不會有任何資料丟失。
  2. 所有對資料庫進行修改的命令都會被記錄到 AOF 重寫快取中。

當子程序完成 AOF 重寫之後, 它會向父程序傳送一個完成訊號, 父程序在接到完成訊號之後, 會呼叫一個訊號處理函式, 並完成以下工作:

  1. 將 AOF 重寫快取中的內容全部寫入到新 AOF 檔案中。
  2. 對新的 AOF 檔案進行改名,覆蓋原有的 AOF 檔案。

當步驟 1 執行完畢之後, 現有 AOF 檔案、新 AOF 檔案和資料庫三者的狀態就完全一致了。

當步驟 2 執行完畢之後, 程式就完成了新舊兩個 AOF 檔案的交替。

這個訊號處理函式執行完畢之後, 主程序就可以繼續像往常一樣接受命令請求了。 在整個 AOF 後臺重寫過程中, 只有最後的寫入快取和改名操作會造成主程序阻塞, 在其他時候, AOF 後臺重寫都不會對主程序造成阻塞, 這將 AOF 重寫對效能造成的影響降到了最低。

以上就是 AOF 後臺重寫, 也即是 BGREWRITEAOF 命令的工作原理。

AOF 後臺重寫的觸發條件

AOF 重寫可以由使用者通過呼叫 BGREWRITEAOF 手動觸發。

另外, 伺服器在 AOF 功能開啟的情況下, 會維持以下三個變數:

  • 記錄當前 AOF 檔案大小的變數 aof_current_size 。
  • 記錄最後一次 AOF 重寫之後, AOF 檔案大小的變數 aof_rewrite_base_size 。
  • 增長百分比變數 aof_rewrite_perc 。

每次當 serverCron 函式執行時, 它都會檢查以下條件是否全部滿足, 如果是的話, 就會觸發自動的 AOF 重寫:

  1. 沒有 BGSAVE 命令在進行。
  2. 沒有 BGREWRITEAOF 在進行。
  3. 當前 AOF 檔案大小大於 server.aof_rewrite_min_size (預設值為 1 MB)。
  4. 當前 AOF 檔案大小和最後一次 AOF 重寫後的大小之間的比率大於等於指定的增長百分比。

預設情況下, 增長百分比為 100% , 也即是說, 如果前面三個條件都已經滿足, 並且當前 AOF 檔案大小比最後一次 AOF 重寫時的大小要大一倍的話, 那麼觸發自動 AOF 重寫。

小結

  • AOF 檔案通過儲存所有修改資料庫的命令來記錄資料庫的狀態。
  • AOF 檔案中的所有命令都以 Redis 通訊協議的格式儲存。
  • 不同的 AOF 儲存模式對資料的安全性、以及 Redis 的效能有很大的影響。
  • AOF 重寫的目的是用更小的體積來儲存資料庫狀態,整個重寫過程基本上不影響 Redis 主程序處理命令請求。
  • AOF 重寫是一個有歧義的名字,實際的重寫工作是針對資料庫的當前值來進行的,程式既不讀寫、也不使用原有的 AOF 檔案。
  • AOF 可以由使用者手動觸發,也可以由伺服器自動觸發。