因為redis是內存數據庫,他把數據都存在內存里,所以要想辦法實現持久化功能。
RDB
RDB持久化可以手動執行,也可以配置定期執行,可以把某個時間的數據狀態保存到RDB文件中,反之,我們可以用RDB文件還原數據庫狀態。
? ? 生成
有兩個命令可以生成RDB文件:
- SAVE?命令由服務器進程直接執行保存操作,所以該命令會阻塞服務器,服務器不能接受其他指令。
- BGSAVE?命令由子進程執行保存操作,所以該命令不會阻塞服務器,服務器可以接受其他指令。。
禁止BGSAVE和SAVE同時執行,也就是說執行其中一個就會拒絕另一個,這是為了避免父進程和子進程同時執行兩個rdbsave,防止產生競爭條件。
? ? 載入
? ? RDB載入工作是服務器啟動時自動執行的。
? ? 自動保存
用戶可以通過save選項設置多個保存條件,服務器狀態中會保存所有用?save
?選項設置的保存條件,當任意一個保存條件被滿足時,服務器會自動執行?BGSAVE?命令。
比如
save 900 1
save 300 10
滿足:服務器在900秒之內被修改至少一次或者300秒內修改至少十次。就會執行BGSAVE。
?
當服務器啟動時,用戶可以通過指定配置文件或者傳入啟動參數來設置save選項,服務器會把條件放到一個結構體里,結構體有一個數組,保存了所有條件。
?
serverCron函數默認100毫秒檢查一次,他會遍歷數組依次檢查,符合條件就會執行BGSAVE。
? ? RDB文件結構
一個完整 RDB 文件所包含的各個部分:
REDIS,
長度5
字節, 保存著?"REDIS"
?五個字符。 通過這五個字符, 可以在載入文件時, 快速檢查載入文件是否 RDB 文件。
db_version
?,長度?4
?字節, 它的值是一個字符串表示的整數, 這個整數記錄了 RDB 文件的版本號
databases
?部分包含著零個或任意多個數據庫, 以及各個數據庫中的鍵值對數據
EOF
?常量的長度為?1
?字節, 這個常量標志著 RDB 文件正文內容的結束
check_sum
?是一個?8
?字節長的無符號整數, 保存著一個校驗和,以此來檢查 RDB 文件是否出錯或損壞
我并不想深入探究databases的組成。就是知道
- RDB 文件是一個經過壓縮的二進制文件,由多個部分組成。
- 對于不同類型的鍵值對, RDB 文件會使用不同的方式來保存它們。
即可。
?
AOF
AOF持久化是通過保存服務器執行的命令來記錄狀態的。還原的時候再執行一遍即可。
功能的實現可以分為命令追加、文件寫入、文件同步三個步驟。
?
當 AOF 持久化功能處于打開狀態時, 服務器在執行完一個寫命令之后, 會以協議格式將被執行的寫命令追加到服務器狀態的?aof_buf
?緩沖區的末尾:
struct redisServer {// ...// AOF 緩沖區sds aof_buf;// ...
};
Redis 服務器進程就是一個事件循環
循環中的文件事件負責接收客戶端的命令請求, 以及向客戶端發送命令回復,
而時間事件則負責執行像?serverCron
?函數這樣需要定時運行的函數。
因為服務器在處理文件事件時可能會執行寫命令, 使得一些內容被追加到?aof_buf
?緩沖區里面, 所以在服務器每次結束一個事件循環之前, 它都會調用?flushAppendOnlyFile
?函數, 考慮是否需要將?aof_buf
?緩沖區中的內容寫入和保存到 AOF 文件里面, 這個過程可以用偽代碼表示:
def eventLoop():while True:# 處理文件事件,接收命令請求以及發送命令回復# 處理命令請求時可能會有新內容被追加到 aof_buf 緩沖區中processFileEvents()# 處理時間事件processTimeEvents()# 考慮是否要將 aof_buf 中的內容寫入和保存到 AOF 文件里面flushAppendOnlyFile()
flushAppendOnlyFile
?函數的行為由服務器配置的?appendfsync
?選項的值來決定
值為?always
?時, 服務器在每個事件循環都要將?aof_buf
?緩沖區中的所有內容寫入到 AOF 文件并且同步 AOF 文件, 所以?always
?的效率最慢的一個, 但從安全性來說,?always
?是最安全的, 因為即使出現故障停機, AOF 持久化也只會丟失一個事件循環中所產生的命令數據。
值為?everysec
?時, 服務器在每個事件循環都要將?aof_buf
?緩沖區中的所有內容寫入到 AOF 文件, 每隔超過一秒就要在子線程中對 AOF 文件進行一次同步: 從效率上來講,?everysec
?模式足夠快, 并且就算出現故障停機, 數據庫也只丟失一秒鐘的命令數據。
值為?no
?時, 服務器在每個事件循環都要將?aof_buf
?緩沖區中的所有內容寫入到 AOF 文件, 至于何時對 AOF 文件進行同步, 則由操作系統控制。
因為處于?no
?模式下的?flushAppendOnlyFile
?調用無須執行同步操作, 所以該模式下的 AOF 文件寫入速度總是最快的, 不過因為這種模式會在系統緩存中積累一段時間的寫入數據, 所以該模式的單次同步時長通常是三種模式中時間最長的: 從平攤操作的角度來看,no
?模式和?everysec
?模式的效率類似, 當出現故障停機時, 使用?no
?模式的服務器將丟失上次同步 AOF 文件之后的所有寫命令數據。
? ? 重寫
AOF持久化是保存了一堆命令來恢復數據庫,隨著時間流逝,存的會越來越多,如果不加以控制,文件過大可能影響服務器甚至計算機。而且文件過大,恢復時需要時間也太長。
所以redis提供了重寫功能,寫出的新文件不會包含任何浪費時間的冗余命令。
接下來,我們就介紹重寫的原理。
其實重寫不會對現有的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條命令。
- 偽代碼表示如下
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(): continueif 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_rewrite函數可以創建新的AOF文件,但是這個函數會進行大量的寫入操作,所以調用這個函數的線程被長時間的阻塞,因為服務器使用單線程來處理命令請求;所以如果直接是服務器進程調用AOF_REWRITE函數的話,那么重寫AOF期間,服務器將無法處理客戶端發送來的命令請求;
Redis不希望AOF重寫會造成服務器無法處理請求,所以將AOF重寫程序放到子進程(后臺)里執行。這樣處理的好處是:?
1)子進程進行AOF重寫期間,主進程可以繼續處理命令請求;
2)子進程帶有主進程的數據副本,使用子進程而不是線程,可以避免在鎖的情況下,保證數據的安全性。
還有一個問題,可能重寫的時候又有新的命令過來,造成信息不對等,所以redis設置了一個緩沖區,重寫期間把命令放到重寫緩沖區。
?
? 總結
AOF重寫的目的是為了解決AOF文件體積膨脹的問題,使用更小的體積來保存數據庫狀態,整個重寫過程基本上不影響Redis主進程處理命令請求;
AOF重寫其實是一個有歧義的名字,實際上重寫工作是針對數據庫的當前狀態來進行的,重寫過程中不會讀寫、也不適用原來的AOF文件;
AOF可以由用戶手動觸發,也可以由服務器自動觸發。
?