Redis是一個鍵值對數據庫服務器,服務器中通常包含着任意個非空數據庫,而每個非空數據庫中又可以包含任意個鍵值對,為了方便起見,我們將服務器中的非空數據庫以及它們的鍵值對統稱為數據庫狀態
舉個例子,圖10-1 展示了一個包含三個非空數據庫的Redis 服務器,這三個數據庫以及數據庫中的鍵值對就是該服務器的數據庫狀態

因為Redis 是內存數據庫,它將自己的數據庫狀態儲存在內存里面,所以如果不想辦法將儲存在內存中的數據庫狀態保存到磁盤里面,那么一旦服務器進程退出,服務器中的數據庫狀態也會消失不見
為了解決這個問題,Redis提供了RDB持久化功能,這個功能可以將Redis 在內存中的數據庫狀態保存到磁盤里面,避免數據意外丟失
RDB持久化既可以手動執行,也可以根據服務器配置選項定期執行,該功能可以將某個時間點上的數據庫狀態保存到一個RDB文件中,如圖10-2所示
RDB 持久化功能所生成的RDB 文件是一個經過壓縮的二進制文件,通過該文件可以還原生成RDB 文件時的數據庫狀態,如圖1 0-3所示

因為RDB文件是保存在硬盤里面的,所以即使Redis服務器進程退出,甚至運行Redis服務器的計算機停機,但只要RDB文件仍然存在,Redis服務器就可以用它來還原數據庫狀態
RDB文件的創建與載入
有兩個Redis命令可以用於生成RDB文件,一個是SAVE,另一個是BGSAVE
SAVE命令會阻塞Redis服務器進程,直到RDB文件創建完畢為止,在服務器進程阻塞期間,服務器不能處理任何命令請求
redis> SAVE //等待直到RDB 文件創建完畢
OK
和SAVE命令直接阻塞服務器進程的做法不同,BGSAVE命令會派生出一個子進程,然后由子進程負責創建RDB 文件,服務器進程(父進程)繼續處理命令請求
redis> BGSAVE //派生子進程,並由子進程創建RDB 文件
Background saving started
創建RDB文件的實際工作由rdb.c/rdbSave函數完成,SAVE命令和BGSAVE命令會以不同的方式調用這個函數,通過以下偽代碼可以明顯地看出這兩個命令之間的區別
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()
和使用SAVE命令或者BGSAVE命令創建RDB文件不同,RDB 文件的載入工作是在服務啟動時自動執行的,所以Redis並沒有專門用於載入RDB文件的命令,只要Redis服務器在啟動時檢測到RDB文件存在,它就會自動載入RDB文件
以下是Redis服務器啟動時打印的日志記錄,其中第二條日志DB loaded from disk就是服務器在成功載入RDB 文件之后打印的
$ redis-server [7379] 30 Aug 21:07:01.270 # Server started, Redis version 2.9.11 [7379] 30 Aug 21:07:01.289 * DB loaded from disk : 0.018 seconds [7379] 30 Aug 21:07:01.289 * Tbe server is now ready to accept connections on port 6379
另外值得一提的是,因為AOF文件的更新頻率通常比RDB 文件的更新頻率高,所以如果服務器開啟了AOF持久化功能,那么服務器會優先使用AOF文件來還原數據庫狀態
只有在AOF持久化功能處於關閉狀態時,服務器才會使用RDB 文件來還原數據庫狀態服務器判斷該用哪個文件來還原數據庫狀態的流程如圖10-4 所示
載入RDB文件的實際工作由rdb.c/rdbLoad函數完成,這個函數和rdbSave函數之間的關系可以用圖10-5表示

SAVE命令執行時的服務器狀態
前面提到過,當SAVE命令執行時,Redis服務器會被阻塞,所以當SAVE命令正在執行時,客戶端發送的所有命令請求都會被拒絕
只有在服務器執行完SAVE命令、重新開始接受命令請求之后,客戶端發送的命令才會被處理
BGSAVE命令執行時的服務器狀態
因為BGSAVE命令的保存工作是由子進程執行的,所以在子進程創建RDB文件的過程中,Redis服務器仍然可以繼續處理客戶端的命令請求,但是,在BGSAVE命令執行期間,服務器處理SAVE、BGSAVE、BGREWRITEAOF三個命令的方式會和平時有所不同
首先,在BGSAVE命令執行期間,客戶端發送的SAVE命令會被服務器拒絕,服務器禁止SAVE命令和BGSAVE命令同時執行是為了避免父進程(服務器進程)和子進程同時執行兩個rdbSave調用,防止產生競爭條件
其次,在BGSAVE命令執行期間,客戶端發送的BGSAVE命令會被服務器拒絕,因為同時執行兩個BGSAVE 命令也會產生競爭條件
最后,BGREWRITEAOF和BGSAVE兩個命令不能同時執行
如果BGSAVE命令正在執行,那么客戶端發送的BGREWRITEAOF命令會被延遲到BGSAVE命令執行完畢之后執行
如果BGREWRITEAOF命令正在執行,那么客戶端發送的BGSAVE命令會被服務器拒絕
因為BGREWRITEAOF和BGSAVE兩個命令的實際工作都由子進程執行,所以這兩個命令在操作方面並沒有什么沖突的地方,不能同時執行它們只是一個性能方面的考慮一一並
發出兩個子進程,井且這兩個子進程都同時執行大量的磁盤寫入操作,這怎么想都不會是一個好主意
RDB 文件載入時的服務器狀態
服務器在載入RDB 文件期間,會一直處於阻塞狀態,直到載入工作完成為止
自動間隔性保存
因為BGSAVE命令可以在不阻塞服務器進程的情況下執行,所以Redis允許用戶通過設置服務器配置的save選項,讓服務器每隔一段時間自動執行一次BGSAVE命令
用戶可以通過save選項設置多個保存條件,但只要其中任意一個條件被滿足,服務器就會執行BGSAVE命令
舉個例子,如果我們向服務器提供以下配置
save 900 1 save 300 10 save 60 10000
那么只要滿足以下三個條件中的任意一個,BGSAVE命令就會被執行
服務器在900秒之內,對數據庫進行了至少1次修改
服務器在300秒之內,對數據庫進行了至少10次修改
服務器在60秒之內,對數據庫進行了至少10000次修改。
舉個例子,以下是Redis服務器在60秒之內,對數據庫進行了至少10000次修改之后,服務器自動執行BGSAVE命令時打印出來的日志:
[5085] 03 Sep 17:09:49.463 * 10000 changes in 60 seconds . Saving ... [5085] 03 Sep 17:09:49.463 * Background saving started by pid 5189 [5189] 03 Sep 17:09:49.522 • DB saved on disk [5189] 03 Sep 17:09:49.522 • RDB: 0 MB of memory used by copy-on-write [5085] 03 Sep 17:09:49.563 • Background saving terminated with succes
設置保存條件
當Redis服務器啟動時,用戶可以通過指定配置文件或者傳人啟動參數的方式設置save選項,如果用戶沒有主動設置save選項,那么服務幫會為save選項設置默認條件:
save 900 1 save 300 10 save 60 10000
接着,服務器程序會根據save選項所設置的保存條件,設置服務器狀態redisServer
結構的saveparams屬性:
struct redisServer { //... //記錄了保存條件的數組 struct saveparam *saveparams; //... };
saveparams屬性是一個數組,數組中的每個元素都是一個saveparam結構,每個
saveparam結構都保存了一個save選項設置的保存條件:
struct saveparam { //秒數 time_t seconds; //修改數 int changes; };
比如說,如果save選項的值為以下條件:
save 900 1 save 300 10 save 60 10000
那么服務器狀態中的saveparams數組將會是圖10-6所示的樣子。

dirty計數器和lastsave屬性
除了saveparams數組之外,服務器狀態還維持着一個dirty計數器,以及一個lastsave屬性:
dirty計數器記錄距離上一次成功執行SAVE命令或者BGSAVE命令之后,服務器對數據庫狀態(服務器中的所有數據庫)進行了多少次修改(包括寫入、刪除、更新等操作)。
lastsave屬性是一個UNIX時間戳,記錄了服務器上一次成功執行SAVE命令或者BGSAVE命令的時間。
當服務器成功執行一個數據庫修改命令之后,程序就會對dirty計數器進行更新:命令修改了多少次數據庫.dirty計數器的值就增加多少。
例如,如果我們為一個字符串鍵設置值:
redis>SET message "hello" OK
那么程序會將dirty計數器的值增加1。
又例如,如果我們向一個集合鍵增加三個新元素:
redis>SADD database Redis MongoDB MariaDB (integer) 3
那么程序會將dirty計數器的值增加3.
