一、Redis的持久化
我們都知道內存就是暫時存儲程序以及數據的地方,存取速率快,那基於內存的Redis當然不會想在停機/故障的時候丟失數據,這個時候就得想辦法將暫時數據存到一個“永久”的地方(磁盤文件中、XML數據文件中),則爲持久化。上一節我們的Redis事務中有提到持久性,就讓我們來探索一下Redis的持久化是怎麼實現的。
二、RDB持久化方式 【快照方式(snapshotting)】
在上圖我們可以看見RDB文件創建的兩個命令分別是SAVE和BGSAVE,實際上這兩個方法都是調用的rdbSave函數完成的。
#以不同的方式來調用rdbSave()
def SAVE():
#創建RDB文件
rdbSave()
def BGSAVE():
#創建子進程
pid = fork()
if pid == 0:
#子進程負責創建RDB文件
rdbSave()
#完成之後向父進程發送信號
signal_parent()
elif pid > 0:
#父進程繼續處理命令請求,並通過輪詢等待子進程的信號
handle_request_and_wait_signal()
else:
#處理出錯情況
handle_fork_error()
據我們所知Redis是可以配置文件或者傳入啓動參數的方式啓動的,所以可以配置save參數來設置自動間隔性保存的屬性。沒有主動設置save選項,那麼服務器會爲save選項設置默認條件:
#服務器在900秒之內,
#對數據庫進行了至少1次修改,即執行BGSAVE命令
save 900 1
#同上
save 300 10
save 60 10000
服務器程序會根據save選項所設置的保存條件,設置服務器狀態redisServer結構的saveparams屬性,saveparams屬性是一個數組,數組中的每個元素都是一個saveparam結構,每個saveparam結構都保存了一個save選項設置的保存條件。
struct redisServer {
// ...
//記錄了保存條件的數組
struct saveparam *saveparams;
// ...
//修改計數器
long long dirty;
//上一次執行保存的時間
time_t lastsave;
};
struct saveparam {
//秒數
time_t seconds;
//修改數
int changes;
};
既然提到了服務器狀態redisServer結構,對於其中的dirty計數器和lastsave的UNIX時間戳得了解一下。
- dirty計數器記錄距離上一次成功執行SAVE命令或者BGSAVE命令之後,服務器對數據庫狀態(服務器中的所有數據庫)進行了多少次修改(包括寫入、刪除、更新等操作)。當服務器成功執行一個數據庫修改命令之後,程序就會對dirty計數器進行更新:命令修改了多少次數據庫,dirty計數器的值就增加多少。
- lastsave屬性是一個UNIX時間戳,記錄了服務器上一次成功執行SAVE命令或者BGSAVE命令的時間。
既然提到了設置保存條件,那問題來了,怎麼知道的符合條件呢?
原來Redis服務器有一個默認100毫秒就執一次的函數serverCron(),該函數用於對正在運行的服務器進行維護,它的其中一項工作就是檢查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()
# ...
OK,那既然已經知道了RDB文件是怎麼樣創建的,接下來讓我們看看RDB文件是怎麼樣的?
關於key_value_pairs部分 我們在前面探索Redis的數據類型的時候已經一一的描述了,這裏如果有需要再補充。
三、AOF持久化方式
3.1:通過保持Redis的寫命令來記錄數據庫狀態的AOF持久化例如通過SET、SADD、RPUSH等寫命令保存到AOF文件中。
我們在本文 的第一張圖中有提到AOF持久化的三個步驟:命令追加、文件寫入、文件同步。
#在Redis的服務器中,命令追加是當AOF持久化功能處於打開狀態時,
#服務器在執行完一個寫命令之後,會以協議格式將被執行的寫命令
#追加到服務器狀態的aof_buf緩衝區的末尾
struct redisServer {
// ...
// AOF緩衝區
sds aof_buf;
// ...
};
文件的寫入與同步主要是依靠Redis的文件事件來進行
#上圖寫入和同步緩衝思路
def eventLoop():
while True:
#處理文件事件,接收命令請求以及發送命令回覆
#處理命令請求時可能會有新內容被追加到 aof_buf緩衝區中
processFileEvents()
#處理時間事件
processTimeEvents()
#考慮是否要將 aof_buf中的內容寫入和保存到 AOF文件裏面
flushAppendOnlyFile()
flushAppendOnlyFile函數的行爲由服務器配置的appendfsync選項的值來決定
3.2:而AOF文件的載入和數據還原息息相關,AOF文件保存了所有寫操作命令,只要將AOF文件讀入系統並重新執行一遍對應的操作,可恢復服務器關閉之前的數據庫狀態。
AOF持久化方式的確好用,但將服務器所有的寫命令存起來會面臨 一個問題,就是隨着操作的不斷增加,AOF文件體積不斷的擴大,爲了解決AOF文件存儲過大的問題,Redis提供了rewrite(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):
#使用HGETALL命令獲取哈希鍵包含的所有鍵值對
field1, value1, field2, value2, ..., fieldN, valueN = HGETALL(key)
#使用HMSET命令重寫哈希鍵
f.write_command(HMSET, key, field1, value1, field2, value2, ..., fieldN, valueN)
def rewrite_set(key);
#使用SMEMBERS命令獲取集合鍵包含的所有元素
elem1, elem2, ..., elemN = SMEMBERS(key)
#使用SADD命令重寫集合鍵
f.write_command(SADD, key, elem1, elem2, ..., elemN)
def rewrite_sorted_set(key):
#使用ZRANGE命令獲取有序集合鍵包含的所有元素
member1, score1, member2, score2, ..., memberN, scoreN = ZRANGE(key, 0, -1, "WITHSCORES")
#使用ZADD命令重寫有序集合鍵
f.write_command(ZADD, key, score1, member1, score2, member2, ..., scoreN, memberN)
def rewrite_expire_time(key):
#獲取毫秒精度的鍵過期時間戳
timestamp = get_expire_time_in_unixstamp(key)
#使用PEXPIREAT命令重寫鍵的過期時間
f.write_command(PEXPIREAT, key, timestamp)
問題
3.服務器停機
如果Redis服務器在執行事務的過程中停機,那麼根據服務器所使用的持久化模式,可能有以下情況出現:
❑如果服務器運行在無持久化的內存模式下,那麼重啓之後的數據庫將是空白的,因此數據總是一致的。
❑如果服務器運行在RDB模式下,那麼在事務中途停機不會導致不一致性,因爲服務器可以根據現有的RDB文件來恢復數據,從而將數據庫還原到一個一致的狀態。如果找不到可供使用的RDB文件,那麼重啓之後的數據庫將是空白的,而空白數據庫總是一致的。
❑如果服務器運行在AOF模式下,那麼在事務中途停機不會導致不一致性,因爲服務器可以根據現有的AOF文件來恢復數據,從而將數據庫還原到一個一致的狀態。如果找不到可供使用的AOF文件,那麼重啓之後的數據庫將是空白的,而空白數據庫總是一致的。
綜上所述,無論Redis服務器運行在哪種持久化模式下,事務執行中途發生的停機都不會影響數據庫的一致性。
事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被保存到永久性存儲介質(比如硬盤)裏面了,即使服務器在事務執行完畢之後停機,執行事務所得的結果也不會丟失。
因爲Redis的事務不過是簡單地用隊列包裹起了一組Redis命令,Redis並沒有爲事務提供任何額外的持久化功能,所以Redis事務的耐久性由Redis所使用的持久化模式決定:
❑當服務器在無持久化的內存模式下運作時,事務不具有耐久性:一旦服務器停機,包括事務數據在內的所有服務器數據都將丟失。
❑當服務器在RDB持久化模式下運作時,服務器只會在特定的保存條件被滿足時,纔會執行BGSAVE命令,對數據庫進行保存操作,並且異步執行的BGSAVE不能保證事務數據被第一時間保存到硬盤裏面,因此RDB持久化模式下的事務也不具有耐久性。
❑當服務器運行在AOF持久化模式下,並且appendfsync選項的值爲always時,程序總會在執行命令之後調用同步(sync)函數,將命令數據真正地保存到硬盤裏面,因此這種配置下的事務是具有耐久性的。
❑當服務器運行在AOF持久化模式下,並且appendfsync選項的值爲everysec時,程序會每秒同步一次命令數據到硬盤。因爲停機可能會恰好發生在等待同步的那一秒鐘之內,這可能會造成事務數據丟失,所以這種配置下的事務不具有耐久性。
❑當服務器運行在AOF持久化模式下,並且appendfsync選項的值爲no時,程序會交由操作系統來決定何時將命令數據同步到硬盤。因爲事務數據可能在等待同步的過程中丟失,所以這種配置下的事務不具有耐久性。
no-appendfsync-on-rewrite配置選項對耐久性的影響
配置選項no-appendfsync-on-rewrite可以配合appendfsync選項爲always或者everysec的AOF持久化模式使用。當no-appendfsync-on-rewrite選項處於打開狀態時,在執行BGSAVE命令或者BGREWRITEAOF命令期間,服務器會暫時停止對AOF文件進行同步,從而儘可能地減少I/O阻塞。但是這樣一來,關於“always模式的AOF持久化可以保證事務的耐久性”這一結論將不再成立,因爲在服務器停止對AOF文件進行同步期間,事務結果可能會因爲停機而丟失。因此,如果服務器打開了no-appendfsync-on-
sudo find / -name '*.aof' # 此命令用於查找系統上所有以aof爲後綴的文件
- redis的主從複製
- redis的哨兵機制
- redis的Cluster集羣
- redis的消息模式
- 分佈式鎖
面試題目:
https://baijiahao.baidu.com/s?id=1625681293986572996&wfr=spider&for=pc