業精於勤,荒於嬉。——韓愈
引導語
AOF是Redis的另外一種持久化方式。簡單來說,AOF就是將Redis服務端執行過的每一條命令都保存到一個文件,這樣當Redis重啟時只要按順序回放這些命令就會恢復到原始狀態。那麼,既然已經有了RDB為什麼還需要AOF呢?
1 AOF持久化方式
我們還是從RDB和AOF的實現方式考慮:RDB保存的是一個時間點的快照,那麼如果Redis出現了故障,丟失的就是從最後一次RDB執行的時間點到故障發生的時間間隔之內產生的數據。如果Redis數據量很大,QPS很高,那麼執行一次RDB需要的時間會相應增加,發生故障時丟失的數據也會增多。
而AOF保存的是一條條命令,理論上可以做到發生故障時只丟失一條命令。但由於作業系統中執行寫文件操作代價很大,Redis提供了配置參數,通過對安全性和性能的折中,我們可以設置不同的策略。
既然AOF數據安全性更高,是否可以只使用AOF呢?為什麼Redis推薦RDB和AOF同時開啟呢?
我們再深入考量一下這兩種實現方式:RDB保存的是最終的數據,是一個最終狀態,而AOF保存的是達到這個最終狀態的過程。很明顯,如果Redis有大量的修改操作,RDB中一個數據的最終態可能會需要大量的命令才能達到,這會造成AOF文件過大並且加載時速度過慢(Redis提供了一種AOF重寫的策略來解決上述問題,後文會詳細描述其實現原理)。
再來考慮一下AOF和RDB文件的加載過程。RDB只需要把相應數據加載到內存並生成相應的數據結構(有些結構如intset、ziplist,保存時直接按字符串保存,所以加載時速度會更快),而AOF文件的加載需要先創建一個偽客戶端,然後把命令一條條發送給Redis服務端,服務端再完整執行一遍相應的命令。根據Redis作者做的測試,RDB 10s~20s能加載1GB的文件,AOF的速度是RDB速度的一半(如果做了AOF重寫會加快)。
因為AOF和RDB各有優缺點,因此Redis一般會同時開啟AOF和RDB。
但假設線上同時配置了RDB和AOF,那麼會帶來如下的兩難選擇:重啟時如果優先加載RDB,加載速度更快,但是數據不是很全;如果優先加載AOF,加載速度會變慢,但是數據會比RDB中的要完整。
能不能結合這兩者的優點呢?答案是AOF和RDB的混合持久化方案。
2 AOF持久化的實現
AOF持久化功能的實現可以分為命令追加(append)、文件寫入、文件同步(sync)三個步驟。
a. 命令追加
當AOF持久化功能處於打開狀態時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到伺服器狀態的aof_buf緩衝區的末尾:
struct redisServer {
// ...
// AOF緩衝區
sds aof_buf;
// ...
};
b. AOF文件的寫入與同步
因為伺服器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區裡面,所以在伺服器每次結束一個事件循環之前,它都會調用flushAppendOnlyFile函數,考慮是否需要將aof_buf緩衝區中的內容寫入和保存到AOF文件裡面。
flushAppendOnlyFile函數的行為由伺服器配置的appendfsync選項的值來決定,各個不同值產生的行為如表11-1所示。
如果用戶沒有主動為appendfsync選項設置值,那麼appendfsync選項的默認值為everysec。
3 AOF持久化的效率和安全性
伺服器配置appendfsync選項的值直接決定AOF持久化功能的效率和安全性。
· 當appendfsync的值為always時,伺服器在每個事件循環都要將aof_buf緩衝區中的所有內容寫入到AOF文件,並且同步AOF文件,所以always的效率是appendfsync選項三個值當中最慢的一個,但從安全性來說,always也是最安全的,因為即使出現故障停機,AOF持久化也只會丟失一個事件循環中所產生的命令數據。
· 當appendfsync的值為everysec時,伺服器在每個事件循環都要將aof_buf緩衝區中的所有內容寫入到AOF文件,並且每隔一秒就要在子線程中對AOF文件進行一次同步。從效率上來講,everysec模式足夠快,並且就算出現故障停機,資料庫也只丟失一秒鐘的命令數據。
· 當appendfsync的值為no時,伺服器在每個事件循環都要將aof_buf緩衝區中的所有內容寫入到AOF文件,至於何時對AOF文件進行同步,則由作業系統控制。因為處於no模式下的flushAppendOnlyFile調用無須執行同步操作,所以該模式下的AOF文件寫入速度總是最快的,不過因為這種模式會在系統緩存中積累一段時間的寫入數據,所以該模式的單次同步時長通常是三種模式中時間最長的。從平攤操作的角度來看,no模式和everysec模式的效率類似,當出現故障停機時,使用no模式的伺服器將丟失上次同步AOF文件之後的所有寫命令數據。
4 AOF文件的載入和數據還原
a. 創建一個不帶網絡連接的偽客戶端(fake client):因為Redis的命令只能在客戶端上下文中執行,而載入AOF文件時所使用的命令直接來源於AOF文件而不是網絡連接,所以伺服器使用了一個沒有網絡連接的偽客戶端來執行AOF文件保存的寫命令,偽客戶端執行命令的效果和帶網絡連接的客戶端執行命令的效果完全一樣。
b. 從AOF文件中分析並讀取出一條寫命令。
c. 使用偽客戶端執行被讀出的寫命令。
d. 一直執行步驟2和步驟3,直到AOF文件中的所有寫命令都被處理完畢為止。
5 AOF重寫
因為AOF持久化是通過保存被執行的寫命令來記錄資料庫狀態的,所以隨著伺服器運行時間的流逝,AOF文件中的內容會越來越多,文件的體積也會越來越大,如果不加以控制的話,體積過大的AOF文件很可能對Redis伺服器、甚至整個宿主計算機造成影響,並且AOF文件的體積越大,使用AOF文件來進行數據還原所需的時間就越多。
為了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫(rewrite)功能。通過該功能,Redis伺服器可以創建一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所保存的資料庫狀態相同,但新AOF文件不會包含任何浪費空間的冗餘命令,所以新AOF文件的體積通常會比舊AOF文件的體積要小得多。
雖然Redis將生成新AOF文件替換舊AOF文件的功能命名為「AOF文件重寫」,但實際上,AOF文件重寫並不需要對現有的AOF文件進行任何讀取、分析或者寫入操作,這個功能是通過讀取伺服器當前的資料庫狀態來實現的。
在實際中,為了避免在執行命令時造成客戶端輸入緩衝區溢出,重寫程序在處理列表、哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數量,如果元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那麼重寫程序將使用多條命令來記錄鍵的值,而不單單使用一條命令。
作為一種輔佐性的維護手段,Redis不希望AOF重寫造成伺服器無法處理請求,所以Redis決定將AOF重寫程序放到子進程裡執行,這樣做可以同時達到兩個目的:
a. 子進程進行AOF重寫期間,伺服器進程(父進程)可以繼續處理命令請求。
b. 子進程帶有伺服器進程的數據副本,使用子進程而不是線程,可以在避免使用鎖的情況下,保證數據的安全性。
重寫時數據一致性問題
使用子進程也有一個問題需要解決,因為子進程在進行AOF重寫期間,伺服器進程還需要繼續處理命令請求,而新的命令可能會對現有的資料庫狀態進行修改,從而使得伺服器當前的資料庫狀態和重寫後的AOF文件所保存的資料庫狀態不一致。
為了解決這種數據不一致問題,Redis伺服器設置了一個AOF重寫緩衝區,這個緩衝區在伺服器創建子進程之後開始使用,當Redis伺服器執行完一個寫命令之後,它會同時將這個寫命令發送給AOF緩衝區和AOF重寫緩衝區,如圖:
6 AOF對過期鍵的處理
當伺服器以AOF持久化模式運行時,如果資料庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那麼AOF文件不會因為這個過期鍵而產生任何影響。
當過期鍵被惰性刪除或者定期刪除之後,程序會向AOF文件追加(append)一條DEL命令,來顯式地記錄該鍵已被刪除。
和生成RDB文件時類似,在執行AOF重寫的過程中,程序會對資料庫中的鍵進行檢查,已過期的鍵不會被保存到重寫後的AOF文件中。
擴展:複製
當伺服器運行在複製模式下時,從伺服器的過期鍵刪除動作由主伺服器控制:
· 主伺服器在刪除一個過期鍵之後,會顯式地向所有從伺服器發送一個DEL命令,告知從伺服器刪除這個過期鍵。
· 從伺服器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期的鍵一樣來處理過期鍵。
· 從伺服器只有在接到主伺服器發來的DEL命令之後,才會刪除過期鍵。
通過由主伺服器來控制從伺服器統一地刪除過期鍵,可以保證主從伺服器數據的一致性,也正是因為這個原因,當一個過期鍵仍然存在於主伺服器的資料庫時,這個過期鍵在從伺服器裡的複製品也會繼續存在。