Redis提供了兩種持久化方式:RDB和AOFpython
下面,咱們來看看上述二者的底層實現原理。redis
在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文件來還原數據庫狀態。併發
在執行save命令時,redis服務器會被阻塞,因此當save命令正在被執行時,客戶端發送的全部命令請求都會被拒絕。app
在執行bgsave命令時,因爲是子進程在處理持久化操做,因此Redis服務器能夠繼續處理客戶的命令請求。可是,在執行bgsave命令期間,若是客戶端又發送來了save,bgsave,bgrewrteaof三個命令其中一個,那麼服務器的處理方式會有所不一樣。函數
首先,bgsave 命令正在被子進程執行,那麼客戶端發來的save命令會直接被服務器拒絕,這是爲了不父進程與子進程同時執行兩個rdbSave()調用,防止產生競爭條件。
其次,bgsave 命令正在被子進程執行,那麼客戶端發來的bgsave命令也會直接被服務器拒絕,一樣也是爲了防止產生競爭條件。
最後,bgsave命令和bgrewrteaof命令不能同時進行,若是bgsave命令正在執行,客戶端的bgrewrteaof命令會延遲到bgsave命令執行完畢之後纔會執行;若是bgrewrteaof命令正在被執行,那麼客戶端的bgsave命令會直接被服務器拒絕。這是由於,這兩個命令都是由子進程來執行的,不能同時執行主要考慮到性能問題,試想兩個併發執行的命令,同時進行大量的讀寫磁盤操做,這會大大下降服務器性能。
上述咱們講到,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。是否是很神奇!!
若是有多個條件同時存在的話,那麼它的結構以下:
除了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命令,進行間隔性數據保存的實現原理。
RDB持久化是經過保存數據庫中的鍵值對來記錄數據庫狀態,而AOF持久化則是經過保存Redis服務器所執行的寫命令來記錄數據狀態(如:set key "hello world" 以RDB持久化方式,文件內容爲key:hello world,以AOF持久化方式,文件內容爲set key "hello world")。
接下來,咱們來看看AOF持久化的實現原理以及減少AOF文件體積的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
由於AOF文件裏面包含了重鍵數據庫狀態所需的全部寫命令,因此服務器只要讀入並從新執行一遍AOF文件裏面保存的寫命令,就能夠還原服務器關閉以前的數據庫狀態。
具體還原過程:
建立一個不帶網絡鏈接的僞客戶端,由於redis命令只能在客戶端上下文中執行,而載入AOF文件所使用的命令直接來源AOF文件而不是網絡鏈接,因此服務器使用了一個僞客戶端來執行AOF文件保存的寫命令,效果與客戶端執行命令同樣。
從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)