文章首發於公衆號「蘑菇睡不着」,歡迎來訪~
你們都知道 Redis 是一個內存數據庫,數據都存儲在內存中,這也是 Redis 很是快的緣由之一。雖然速度提上來了,可是若是數據一直放在內存中,是很是容易丟失的。好比 服務器關閉或宕機了,內存中的數據就木有了。爲了解決這一問題,Redis 提供了 持久化 機制。分別是 RDB 以及 AOF 持久化。c++
RDB 持久化能夠在指定的時間間隔內生成數據集的時間點快照(point-in-time snapshot)。
redis
RDB 在重啓保存了大數據集的實例比 AOF 快。數據庫
有個兩個 Redis 命令能夠用於生成 RDB 文件,一個是 SAVE,另外一個是 BGSAVE。
SAVE 命令會阻塞 Redis 服務器進程,直到 RDB 文件建立完畢爲止,在服務器進程阻塞期間,服務器不能處理任何命令請求。數組
> SAVE // 一直等到 RDB 文件建立完畢 OK
和 SAVE 命令直接阻塞服務器進程不一樣的是,BGSAVE 命令會派生出一個子進程,而後由子進程負責建立 RDB 文件,服務器進程(父進程)繼續處理命令進程。
執行fork的時候操做系統(類Unix操做系統)會使用寫時複製(copy-on-write)策略,即fork函數發生的一刻父子進程共享同一內存數據,當父進程要更改其中某片數據時(如執行一個寫命令 ),操做系統會將該片數據複製一份以保證子進程的數據不受影響,因此新的RDB文件存儲的是執行fork一刻的內存數據。安全
> BGSAVE // 派生子進程,並由子進程建立 RDB 文件 Background saving started
生成 RDB 文件由兩種方式:一種是手動,就是上邊介紹的用命令的方式;另外一種是自動的方式。
接下來詳細介紹一下自動生成 RDB 文件的流程。
Redis 容許用戶經過設置服務器配置的 save 選項,讓服務器每隔一段時間自動執行一次 BGSAVE 命令。
用戶能夠經過在 redis.conf 配置文件中的 SNAPSHOTTING 下 save 選項設置多個保存條件,但只要其中任意一個條件被知足,服務器就會執行 BGSAEVE 命令。
如,如下配置:
save 900 1
save 300 10
save 60 10000
上邊三個配置的含義是:服務器
若是沒有手動去配置 save 選項,那麼服務器會爲 save 選項配置默認參數:
save 900 1
save 300 10
save 60 10000
接着,服務器就會根據 save 選項的配置,去設置服務器狀態 redisServer 結構的 saveparams 屬性:網絡
struct redisServer{ // ... // 記錄了保存條件的數組 struct saveparams *saveparams; // ... };
saveparams 屬性是一個數組,數組中的每個元素都是一個 saveparam 結構,每一個 saveparam 結構都保存了一個 save 選項設置的保存條件:app
struct saveparam { // 秒數 time_t seconds; // 修改數 int changes; };
除了 saveparams 數組以外,服務器狀態還維持着一個 dirty 計數器,以及一個 lastsave 屬性;async
struct redisServer { // ... // 修改計數器 long long dirty; // 上一次執行保存時間 time_t lastsave; // ... }
Redis 的服務器週期性操做函數 serverCron 默認每隔 100 毫秒執行一次,該函數用於對正在運行的服務器進行維護,它的其中一項工做就是檢查 save 選項所設置的保存條件是否已經知足,若是知足的話就執行 BGSAVE 命令。
Redis serverCron 源碼解析以下:函數
程序會遍歷並檢查 saveparams 數組中的全部保存條件,只要有任意一個條件被知足,服務器就會執行 BGSAVE 命令。
下面是 rdbSaveBackground 的源碼流程:
下圖展現了一個完整 RDB 文件所包含的各個部分。
redis 文件的最開頭是 REDIS 部分,這個部分的長度是 5 字節,保存着 「REDIS」 五個字符。經過這五個字符,程序能夠在載入文件時,快速檢查所載入的文件是否時 RDB 文件。
db_version 長度爲 4 字節,他的值時一個字符串表示的整數,這個整數記錄了 RDB 文件的版本號,好比 「0006」 就表明 RDB 文件的版本爲第六版。
database 部分包含着零個或任意多個數據庫,以及各個數據庫中的鍵值對數據:
EOF 常量的長度爲 1 字節,這個常量標誌着 RDB 文件正文內容的結束,當讀入程序遇到這個值後,他知道全部數據庫的全部鍵值對已經載入完畢了。
check_sum 是一個 8 字節長的無符號整數,保存着一個校驗和,這個校驗和時程序經過對 REDIS、db_version、database、EOF 四個部分的內容進行計算得出的。服務器在載入 RDB 文件時,會將載入數據所計算出的校驗和與 check_sum 所記錄的校驗和進行對比,以此來檢查 RDB 是否有出錯或者損壞的狀況。
舉個例子:下圖是一個 0 號數據庫和 3 號數據庫的 RDB 文件。第一個就是 「REDIS」 表示是一個 RDB 文件,以後的 「0006」 表示這是第六版的 REDIS 文件,而後是兩個數據庫,以後就是 EOF 結束標識符,最後就是 check_sum。
AOF持久化方式記錄每次對服務器寫的操做,當服務器重啓的時候會從新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操做到文件末尾.Redis還能對AOF文件進行後臺重寫,使得AOF文件的體積不至於過大.
AOF持久化功能的實現能夠分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。
當 AOF 持久化功能處於打開狀態時,服務器在執行完一個寫命令以後,會以協議格式將被執行的寫命令追加到服務器狀態的 aof_buf 緩衝區的末尾。
struct redisServer { // ... // AOF 緩衝區 sds aof_buf; // .. };
若是客戶端向服務器發送如下命令:
> set KEY VALUE OK
那麼服務器在執行這個 set 命令以後,會將如下協議內容追加到 aof_buf 緩衝區的末尾;
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
Redis的服務器進程就是一個事件循環(loop),這個循環中的文件事件負責接收客戶端
的命令請求,以及向客戶端發送命令回覆,而時間事件則負責執行像 serverCron 函數這樣需
要定時運行的函數。
由於服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區
裏面,因此在服務器每次結束一個事件循環以前,它都會調用 flushAppendOnlyFile 函數,考
慮是否須要將aof_buf緩衝區中的內容寫入和保存到AOF文件裏面,這個過程能夠用如下僞代
碼錶示:
def eventLoop(): while True: #處理文件事件,接收命令請求以及發送命令回覆 #處理命令請求時可能會有新內容被追加到 aof_buf緩衝區中 processFileEvents() #處理時間事件 processTimeEvents() #考慮是否要將 aof_buf中的內容寫入和保存到 AOF文件裏面 flushAppendOnlyFile()
flushAppendOnlyFile函數的行爲由服務器配置的 appendfsync 選項的值來決定,各個不一樣
值產生的行爲以下表所示。
appendfsync 選項的值 | flushAppendOnlyFile 函數的行爲 |
---|---|
always | 將 aof_buf 緩衝區中的全部內容寫入並同步到 AOF 文件 |
everysec | 將 aof_buf 緩衝區中的全部內容寫入到 AOF 文件,若是上次同步 AOF 文件的時間距離如今超過一秒鐘,那麼再次對 AOF 文件進行同步,而且這個同步操做是由一個線程專門負責執行的 |
no | 將 aof_buf 緩衝區中的全部內容寫入到 AOF 文件,但並不對 AOF 文件進行同步,什麼時候同步由操做系統來決定 |
若是用戶沒有主動爲appendfsync選項設置值,那麼appendfsync選項的默認值爲everysec。
寫到這裏有的小夥伴可能會對上面說的寫入和同步含義弄混,這裏說一下:
寫入:將 aof_buf 中的數據寫入到 AOF 文件中。
同步:調用 fsync 以及 fdatasync 函數,將 AOF 文件中的數據保存到磁盤中。
通俗地講就是,你要往一個文件寫東西,寫的過程就是寫入,而同步則是將文件保存,數據落到磁盤上。
你們以前看文章的時候是否是大多都說 AOF 最多丟失一秒鐘的數據,那是由於 redis AOF 默認是 everysec 策略,這個策略每秒執行一次,因此 AOF 持久化最多丟失一秒鐘的數據。
由於AOF文件裏面包含了重建數據庫狀態所需的全部寫命令,因此服務器只要讀入並從新執行一遍AOF文件裏面保存的寫命令,就能夠還原服務器關閉以前的數據庫狀態。 Redis讀取AOF文件並還原數據庫狀態的詳細步驟以下:
由於AOF持久化是經過保存被執行的寫命令來記錄數據庫狀態的,因此隨着服務器運行 時間的流逝,AOF文件中的內容會愈來愈多,文件的體積也會愈來愈大,若是不加以控制的 話,體積過大的AOF文件極可能對Redis服務器、甚至整個宿主計算機形成影響,而且AOF文 件的體積越大,使用AOF文件來進行數據還原所需的時間就越多。
如 客戶端執行了如下命令是:
> rpush list "A" "B" OK > rpush list "C" OK > rpush list "D" OK > rpush list "E" "F" OK
那麼光是爲了記錄這個list鍵的狀態,AOF文件就須要保存四條命令。
對於實際的應用程度來講,寫命令執行的次數和頻率會比上面的簡單示例要高得多,所 以形成的問題也會嚴重得多。 爲了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫(rewrite)功能。經過該 功能,Redis服務器能夠建立一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所 保存的數據庫狀態相同,但新AOF文件不會包含任何浪費空間的冗餘命令,因此新AOF文件 的體積一般會比舊AOF文件的體積要小得多。 在接下來的內容中,咱們將介紹AOF文件重寫的實現原理,以及BGREWEITEAOF命令 的實現原理。
雖然Redis將生成新AOF文件替換舊AOF文件的功能命名爲「AOF文件重寫」,但實際上, AOF文件重寫並不須要對現有的AOF文件進行任何讀取、分析或者寫入操做,這個功能是通 過讀取服務器當前的數據庫狀態來實現的。
就像上面的狀況,服務器徹底能夠將這六條命令合併成一條。
> rpush list "A" "B" "C" "D" "E" "F"
除了上面列舉的列表鍵以外,其餘全部類型的鍵均可以用一樣的方法去減小 AOF文件中的命令數量。首先從數據庫中讀取鍵如今的值,而後用一條命令去記錄鍵值對,代替以前記錄這個鍵值對的多條命令,這就是AOF重寫功能的實現原理。
在實際中,爲了不在執行命令時形成客戶端輸入緩衝區溢出,重寫程序在處理列表、 哈希表、集合、有序集合這四種可能會帶有多個元素的鍵時,會先檢查鍵所包含的元素數 量,若是元素的數量超過了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那 麼重寫程序將使用多條命令來記錄鍵的值,而不僅僅使用一條命令。 在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值爲64,這也就是 說,若是一個集合鍵包含了超過64個元素,那麼重寫程序會用多條SADD命令來記錄這個集 合,而且每條命令設置的元素數量也爲64個。
AOF 重寫會執行大量的寫操做,這樣會影響主線程,因此redis AOF 重寫放到了子進程去執行。這樣能夠達到兩個目的:
可是有一個問題,當子進程重寫數據時,主進程依然在處理新的數據,這也就會形成數據不一致狀況。
爲了解決這種數據不一致問題,Redis服務器設置了一個AOF重寫緩衝區,這個緩衝區在 服務器建立子進程以後開始使用,當Redis服務器執行完一個寫命令以後,它會同時將這個寫 命令發送給AOF緩衝區和AOF重寫緩衝區,以下圖:
這也就是說,在子進程執行AOF重寫期間,服務器進程須要執行如下三個工做:
這樣一來能夠保證:
當子進程完成AOF重寫工做以後,它會向父進程發送一個信號,父進程在接到該信號之 後,會調用一個信號處理函數,並執行如下工做:
這個信號處理函數執行完畢以後,父進程就能夠繼續像往常同樣接受命令請求了。
在整個AOF後臺重寫過程當中,只有信號處理函數執行時會對服務器進程(父進程)形成 阻塞,在其餘時候,AOF後臺重寫都不會阻塞父進程,這將AOF重寫對服務器性能形成的影 響降到了最低。
Redis 還能夠同時使用 AOF 持久化和 RDB 持久化。 在這種狀況下, 當 Redis 重啓時, 它會優先使用 AOF 文件來還原數據集, 由於 AOF 文件保存的數據集一般比 RDB 文件所保存的數據集更完整。可是 AOF 恢復比較慢,Redis 4.0 推出了混合持久化。
混合持久化: 將 rdb 文件的內容和增量的 AOF 日誌文件存在一塊兒。這裏的 AOF 日誌再也不是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,一般這部分 AOF 日誌很小。
因而在 Redis 重啓的時候,能夠先加載 RDB 的內容,而後再重放增量 AOF 日誌就能夠徹底替代以前的 AOF 全量文件重放,重啓效率所以大幅獲得提高。
以爲文章不錯的話,小夥伴們麻煩點個贊、關個注、轉個發一下唄~你的支持就是我寫文章的動力。
更多精彩的文章請關注公衆號「蘑菇睡不着」。
你越主動就會越主動,咱們下期見~