Redis持久化

Redis提供了兩種持久化方式:RDB和AOFpython

  下面,咱們來看看上述二者的底層實現原理。redis

1、RDB持久化

1.RDB文件的建立與載入

  在Redis中,有兩種方式能夠生成RDB文件,一個是SAVE,另外一個是BGSAVE數據庫

  二者的主要區別是:SAVE命令在進行持久化操做的過程當中,會阻塞Redis服務進行,也就是說,在以SAVE方式進行持久化操做的過程當中,服務器不能再處理其餘的命令請求,這個請求過程必須等到持久化操做結束;BGSAVE命令則是單獨開啓一個子進程來處理持久化操做。數組

  上述過程用僞代碼表現形式以下:服務器

def save():
    rdbSave() # 將數據寫入文件操做
def bgsave():
    # 建立子進程
    pid = fork()

    if pid == 0:
        # 子進程負責建立RDB文件
        rdbSave()
        # 完成以後向父進程發送信號
        signal_parent()

    elif pid > 0:
        # 父進程繼續處理命令請求,並經過輪詢等待子進程信號
        handle_request_and_wait_signal()
    else:
        # 處理出錯狀況
        handle_fork_error()

  RDB文件的載入是在Redis服務器啓動時,自動載入的,因此Redis並無專門用於載入RDB文件的命令。只要服務器檢測到有RDB文件的存在,它就會自動進行載入 操做。網絡

  關於RDB文件載入過程,值得提一下就是,若是服務器開啓了AOF持久化功能,那麼服務器會優先使用AOF文件來還原數據庫。架構

  只有在AOF持久化功能處於關閉狀態,Redis服務器纔會使用RDBRDB文件來還原數據庫狀態。併發

2.執行save,bgsave命令時,服務器的狀態

  在執行save命令時,redis服務器會被阻塞,因此當save命令正在被執行時,客戶端發送的全部命令請求都會被拒絕。app

  在執行bgsave命令時,因爲是子進程在處理持久化操做,因此Redis服務器能夠繼續處理客戶的命令請求。可是,在執行bgsave命令期間,若是客戶端又發送來了save,bgsave,bgrewrteaof三個命令其中一個,那麼服務器的處理方式會有所不一樣。函數

  首先,bgsave 命令正在被子進程執行,那麼客戶端發來的save命令會直接被服務器拒絕,這是爲了不父進程與子進程同時執行兩個rdbSave()調用,防止產生競爭條件。

  其次,bgsave 命令正在被子進程執行,那麼客戶端發來的bgsave命令也會直接被服務器拒絕,一樣也是爲了防止產生競爭條件。

  最後,bgsave命令和bgrewrteaof命令不能同時進行,若是bgsave命令正在執行,客戶端的bgrewrteaof命令會延遲到bgsave命令執行完畢之後纔會執行;若是bgrewrteaof命令正在被執行,那麼客戶端的bgsave命令會直接被服務器拒絕。這是由於,這兩個命令都是由子進程來執行的,不能同時執行主要考慮到性能問題,試想兩個併發執行的命令,同時進行大量的讀寫磁盤操做,這會大大下降服務器性能。

3.間隔性保存

  上述咱們講到,save命令會阻塞服務器進程,而bgsave命令則會另啓一個進程來執行持久化操做。

  由於bgsave命令能夠在不阻塞服務器進程來進行持久化,因此redis容許用戶經過設置服務器配置的save選項,來讓redis間接性的自動執行bgsave命令。

  用戶能夠在redis.conf文件配置save保存規則,只要其中一個條件知足,服務器就會自動執行bgsave命令。

save 900 1 # 900秒以內,對數據庫進行了一次修改就執行bgsave命令
save 300 10 # 300秒以內,對數據庫進行了十次修改就執行bgsave命令
save 60 10000 # 60秒以內,對數據庫進行了一萬次修改就執行bgsave命令

  接下來,咱們來看看服務器是如何根據上述配置的規則,自動執行bgsave命令。

  咱們來看看源碼redis.h/redisServer,在這個大的結構體中存在以下一個字段:

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

  服務器會根據save選項所設置的保存條件,將該值設置到服務器redisServer結構的saveparams屬性:

  saveparams屬性是一個數組,數組每個元素都是一個saveparam結構,每一個結構都保存了一個save選擇設置的保存條件:

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

  上述結構體中的兩個參數就是咱們設置的,如:save 600 1; 那麼seconds=600,changes=1。是否是很神奇!!

  若是有多個條件同時存在的話,那麼它的結構以下:

  img

  除了saveparms數組以外,服務器還維持着兩個參數:dirty和lastsave.

  其中,dirty記錄上一次執行save或者bgsave命令,服務器對數據庫狀態進行了多少次修改。lastsave則記錄上一次執行save或者bgsave命令的時間。

struct redisServer{
    // 修改計數器
    long long dirty;

    // 上一次執行保存的時間
    time_t lastsave;
    
    struct saveparam *saveparams; //記錄了保存條件的數組
};

  說完了上述,接下來就來講說,redis服務器是如何發現該執行保存操做呢?

  在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()
        # ...

  以上就是redis服務器根據save選項所設置的保存條件,自動執行bgsave命令,進行間隔性數據保存的實現原理。

二,AOF持久化

   RDB持久化是經過保存數據庫中的鍵值對來記錄數據庫狀態,而AOF持久化則是經過保存Redis服務器所執行的寫命令來記錄數據狀態(如:set key "hello world" 以RDB持久化方式,文件內容爲key:hello world,以AOF持久化方式,文件內容爲set key "hello world")。

   接下來,咱們來看看AOF持久化的實現原理以及減少AOF文件體積的AOF文件重寫實現原理。

1.AOF持久化實現

    這裏,咱們先說說AOF持久化操做,寫入文件的操做並非單單將命令寫入,如set key "hello world",而是將命令按照某種格式進行寫入,至於爲何要這樣作,後面咱們再說。寫入文件的內容以某個格式,咱們稱爲協議格式。如上面的命令,則寫入文件的以下:*2\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nhello\r\n$5\r\nworld

   AOF持久化分爲三個步驟:命令追加,文件寫入,文件同步

命令追加

   當AOF持久化功能處於打開狀態,服務器在執行完一個寫命令以後,會以協議格式的形式將被執行的命令追加到服務器aof_buf緩衝區,至於爲何要寫入,後面介紹。  

struct redisServer{
    sds aof_buf; // 寫入緩衝區
};

文件寫入與同步

   Redis是單線程架構,也就是說redis服務進程處於一個事件循環中,這個事件循環負責接受來自客戶端的命令,以及向客戶端發送命令,而時間事件則負責想serverCron函數這樣須要定時運行的函數。

   由於服務器在處理文件事件時,可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區裏面,因此在服務器每次結束一個事件循環,它都會調用flushAppendOnlyFile函數,考慮是否須要將aof_bug緩衝區中的內容寫入和保存到AOF文件裏面,這個過程可用以下代碼描述:   

def event_loop():
    while True:
        # 處理文件事件,接收命令請求以及發送命令回覆
        # 處理命令請求時可能會有新內容被追加到aof_buf緩衝區中
        processFileEvents()

        # 處理函時間事件
        processTimeEvents()

        # 考慮是否將aof_buf中的內容寫入和保存到AOF文件裏面

        flushAppendOnlyFile()

  而flushAppendOnlyFile函數行爲由服務器配置redis.conf中的appendsync選項的值來決定。

  appendsync=always/everysec(默認)/no

2.AOF文件的載入與數據還原

  由於AOF文件裏面包含了重鍵數據庫狀態所需的全部寫命令,因此服務器只要讀入並從新執行一遍AOF文件裏面保存的寫命令,就能夠還原服務器關閉以前的數據庫狀態。

​ 具體還原過程:

  建立一個不帶網絡鏈接的僞客戶端,由於redis命令只能在客戶端上下文中執行,而載入AOF文件所使用的命令直接來源AOF文件而不是網絡鏈接,因此服務器使用了一個僞客戶端來執行AOF文件保存的寫命令,效果與客戶端執行命令同樣。

  從AOF文件中分析並讀取一條寫命令。

  使用僞客戶端執行被讀出的命令。

  重複上述步驟。

3.AOF重寫 

  由於AOF持久化是經過保存被執行的寫命令來記錄數據庫狀態的,因此隨着服務器運行時間的流逝,AOF文件中的內容愈來愈多,文件的體積也會愈來愈大,若是不加以控制的話,過大的AOF文件可能對Redis服務器,甚至整個宿主計算機形成影響,而且AOF文件的體積越大,使用AOF文件來進行數據還原所需的時間就越多。

  如:     >rpush list 'a' 'b'     >rpush list 'c'     >rpush list 'd'     >rpush list 'e'

  上述光是記錄list狀態,AOF文件就要保存五條命令。爲了解決上述問題,Redis提供了AOF文件重寫功能。

  AOF文件重寫並不須要對現有的AOF文件進行任何讀取操做,而是根據現有的數據庫狀態,將其再次進行持久化操做,而後替換保存以前的文件。

  例如上述四條命令是文件記錄的,將其還原到redis數據,那麼保存在redis數據庫中的是以下情景list-->['a','b','c','d','e'],如今咱們要進行重寫,則根據數據構造出命令:rpush list 'a' 'b' 'c' 'd' 'e'。這樣我經過1條命令來代替上面的4條命令,從而大大節約了空間。這就是AOF文件重寫功能。

  整個重寫過程可用以下僞代碼表示:

def aof_rewrite(new_aof_file_name):

    # 建立新AOF文件
    f = create_file(new_aof_file_name)

    # 遍歷數據庫
    for db in redisServer.db:
        # 忽略空數據庫
        if db.is_empty():continue

        # 寫入Select命令,指定數據號碼
        f.write_command("SELECT" + db.id)

        # 遍歷數據庫中的全部鍵
        for key in db:
            # 忽略已過時的鍵
            if key.is_expired():continue

            # 根據鍵的類型對鍵進行重寫
            if key.type == String:
                rewrite_string(key)
            elif key.type == List:
                rewrite_list(key)
            elif key.type == Hash:
                rewrite_hash(key)
            elif key.type == Set:
                rewrite_set(key)
            elif key.type == SortedSet:
                rewrite_sorted_set(key)

            # 若是鍵帶有過時時間,那麼過時時間也要被重寫
            if key.have_expire_time():
                rewrite_expire_time(key)
    f.close()

def rewrite_string(key):
    # 使用GET命令獲取字符串鍵的值
    value = GET(key)

    # 使用SET命令重寫字符串鍵
    f.write_command(SET,key,value)

def rewrite_list(key):
    # 使用LRANGE命令獲取列表鍵包含的全部元素
    item1,item2,...,itemN = LRANGE(key,0,-1)

    # 使用RPUSH命令重寫列表鍵
    f.write_command(RPUSH,key,item1,item2,....,itemN)

def rewrite_hash(key):
    field1,value1,field2,value2,...,fieldN,valueN = HGETALL(key)
    f.write_command(HSET,key,field1,value1,field2,value2,...,fieldN,valueN)

def rewrite_set(key):
    elem1,elem2,...,elemN = SMEMBERS(key)
    f.write_command(SADD,key,elem1,elem2,...,elemN)

def rewrite_sorted_set(key):
    member1,score1,member2,score2,...,memberN,scoreN = ZRANGE(key,0,-1,"WITHSCORES")
    f.write_command(member1,score1,member2,score2,...,memberN,scoreN)

def rewrite_expire_time(key):
    timestamp = get_expire_time_in_unixstamp(key)
    f.write_command(pexpireat,key,timestamp)
相關文章
相關標籤/搜索