Redis之AOF重寫及其實現原理


AOF 重寫

  • AOF 持久化是通過保存被執行的寫命令來記錄數據庫狀態的,所以AOF文件的大小隨着時間的流逝一定會越來越大;影響包括但不限於:對於Redis服務器,計算機的存儲壓力;AOF還原出數據庫狀態的時間增加;
  • 爲了解決AOF文件體積膨脹的問題,Redis提供了AOF重寫功能:Redis服務器可以創建一個新的AOF文件來替代現有的AOF文件,新舊兩個文件所保存的數據庫狀態是相同的,但是新的AOF文件不會包含任何浪費空間的冗餘命令,通常體積會較舊AOF文件小很多。

AOF 文件重寫的實現

  • AOF重寫並不需要對原有AOF文件進行任何的讀取,寫入,分析等操作,這個功能是通過讀取服務器當前的數據庫狀態來實現的。
 # 假設服務器對鍵list執行了以下命令s;
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> LPOP list
"B"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "C"
2) "D"
3) "E"
4) "F"
5) "G"
127.0.0.1:6379> 
  • 當前列表鍵list在數據庫中的值就爲["C", "D", "E", "F", "G"]。要使用盡量少的命令來記錄list鍵的狀態,最簡單的方式不是去讀取和分析現有AOF文件的內容,,而是直接讀取list鍵在數據庫中的當前值,然後用一條RPUSH list "C" "D" "E" "F" "G"代替前面的6條命令。

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()
  • 實際爲了避免執行命令時造成客戶端輸入緩衝區溢出,重寫程序在處理list hash set zset時,會檢查鍵所包含的元素的個數,如果元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那麼重寫程序會使用多條命令來記錄鍵的值,而不是單使用一條命令。該常量默認值是64– 即每條命令設置的元素的個數 是最多64個,使用多條命令重寫實現集合鍵中元素數量超過64個的鍵;

AOF後臺重寫

  • aof_rewrite函數可以創建新的AOF文件,但是這個函數會進行大量的寫入操作,所以調用這個函數的線程將被長時間的阻塞,因爲Redis服務器使用單線程來處理命令請求;所以如果直接是服務器進程調用AOF_REWRITE函數的話,那麼重寫AOF期間,服務器將無法處理客戶端發送來的命令請求;
  • Redis不希望AOF重寫會造成服務器無法處理請求,所以Redis決定將AOF重寫程序放到子進程(後臺)裏執行。這樣處理的最大好處是:
    • 子進程進行AOF重寫期間,主進程可以繼續處理命令請求;
    • 子進程帶有主進程的數據副本,使用子進程而不是線程,可以避免在鎖的情況下,保證數據的安全性

使用子進程進行AOF重寫的問題

  • 子進程在進行AOF重寫期間,服務器進程還要繼續處理命令請求,而新的命令可能對現有的數據進行修改,這會讓當前數據庫的數據和重寫後的AOF文件中的數據不一致。

如何修正

  • 爲了解決這種數據不一致的問題,Redis增加了一個AOF重寫緩存,這個緩存在fork出子進程之後開始啓用,Redis服務器主進程在執行完寫命令之後,會同時將這個寫命令追加到AOF緩衝區和AOF重寫緩衝區
  • 即子進程在執行AOF重寫時,主進程需要執行以下三個工作:
    • 執行client發來的命令請求;
    • 將寫命令追加到現有的AOF文件中;
    • 將寫命令追加到AOF重寫緩存中。

服務器同時將命令發送到AOF文件和AOF重寫緩衝區

效果

  • 可以保證:
    • AOF緩衝區的內容會定期被寫入和同步到AOF文件中,對現有的AOF文件的處理工作會正常進行
    • 從創建子進程開始,服務器執行的所有寫操作都會被記錄到AOF重寫緩衝區中;

完成AOF重寫之後

  • 當子進程完成對AOF文件重寫之後,它會向父進程發送一個完成信號,父進程接到該完成信號之後,會調用一個信號處理函數,該函數完成以下工作:

    • 將AOF重寫緩存中的內容全部寫入到新的AOF文件中;這個時候新的AOF文件所保存的數據庫狀態和服務器當前的數據庫狀態一致;
    • 對新的AOF文件進行改名,原子的覆蓋原有的AOF文件;完成新舊兩個AOF文件的替換。
  • 當這個信號處理函數執行完畢之後,主進程就可以繼續像往常一樣接收命令請求了。在整個AOF後臺重寫過程中,只有最後的“主進程寫入命令到AOF緩存”和“對新的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重寫操作:

    • 沒有BGSAVE命令(RDB持久化)/AOF持久化在執行;
    • 沒有BGREWRITEAOF在進行;
    • 當前AOF文件大小要大於server.aof_rewrite_min_size(默認爲1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
    • 當前AOF文件大小和最後一次重寫後的大小之間的比率等於或者等於指定的增長百分比(在配置文件設置了auto-aof-rewrite-percentage參數,不設置默認爲100%)

如果前面三個條件都滿足,並且當前AOF文件大小比最後一次AOF重寫時的大小要大於指定的百分比,那麼觸發自動AOF重寫。


總結

  • AOF重寫的目的是爲了解決AOF文件體積膨脹的問題,使用更小的體積來保存數據庫狀態,整個重寫過程基本上不影響Redis主進程處理命令請求;
  • AOF重寫其實是一個有歧義的名字,實際上重寫工作是針對數據庫的當前狀態來進行的,重寫過程中不會讀寫、也不適用原來的AOF文件;
  • AOF可以由用戶手動觸發,也可以由服務器自動觸發。

ref:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章