Redis 的數據所有在內存裏,若是忽然宕機,數據就會所有丟失,所以必須有一種機制來保證 Redis 的數據不會由於故障而丟失,這種機制就是 Redis 的持久化機制。程序員
Redis 的持久化機制有兩種,第一種是快照,第二種是 AOF 日誌。快照是一次全量備份,AOF 日誌是連續的增量備份。快照是內存數據的二進制序列化形式,在存儲上很是緊湊,而 AOF 日誌記錄的是內存數據修改的指令記錄文本。AOF 日誌在長期的運行過程當中會變的無比龐大,數據庫重啓時須要加載 AOF 日誌進行指令重放,這個時間就會無比漫長。因此須要按期進行 AOF 重寫,給 AOF 日誌進行瘦身。web
咱們知道 Redis 是單線程程序,這個線程要同時負責多個客戶端套接字的併發讀寫操做和內存數據結構的邏輯讀寫。redis
在服務線上請求的同時,Redis 還須要進行內存快照,內存快照要求 Redis 必須進行文件 IO 操做,可文件 IO 操做是不能使用多路複用 API。數據庫
這意味着單線程同時在服務線上的請求還要進行文件 IO 操做,文件 IO 操做會嚴重拖垮服務器請求的性能。還有個重要的問題是爲了避免阻塞線上的業務,就須要邊持久化邊響應客戶端請求。持久化的同時,內存數據結構還在改變,好比一個大型的 hash 字典正在持久化,結果一個請求過來把它給刪掉了,還沒持久化完呢,這尼瑪要怎麼搞?緩存
那該怎麼辦呢?安全
Redis 使用操做系統的多進程 COW(Copy On Write) 機制來實現快照持久化,這個機制頗有意思,也不多人知道。多進程 COW 也是鑑定程序員知識廣度的一個重要指標。服務器
Redis 在持久化時會調用 glibc 的函數fork
產生一個子進程,快照持久化徹底交給子進程來處理,父進程繼續處理客戶端請求。子進程剛剛產生時,它和父進程共享內存裏面的代碼段和數據段。這時你能夠將父子進程想像成一個連體嬰兒,共享身體。這是 Linux 操做系統的機制,爲了節約內存資源,因此儘量讓它們共享起來。在進程分離的一瞬間,內存的增加幾乎沒有明顯變化。數據結構
用 Python 語言描述進程分離的邏輯以下。fork
函數會在父子進程同時返回,在父進程裏返回子進程的 pid,在子進程裏返回零。若是操做系統內存資源不足,pid 就會是負數,表示fork
失敗。併發
pid = os.fork()
if pid > 0: handle_client_requests() # 父進程繼續處理客戶端請求 if pid == 0: handle_snapshot_write() # 子進程處理快照寫磁盤 if pid < 0: # fork error
子進程作數據持久化,它不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,而後序列化寫到磁盤中。可是父進程不同,它必須持續服務客戶端請求,而後對內存數據結構進行不間斷的修改。異步
這個時候就會使用操做系統的 COW 機制來進行數據段頁面的分離。數據段是由不少操做系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面複製一份分離出來,而後對這個複製的頁面進行修改。這時子進程相應的頁面是沒有變化的,仍是進程產生時那一瞬間的數據。
隨着父進程修改操做的持續進行,愈來愈多的共享頁面被分離出來,內存就會持續增加。可是也不會超過原有數據內存的 2 倍大小。另一個 Redis 實例裏冷數據佔的比例每每是比較高的,因此不多會出現全部的頁面都會被分離,被分離的每每只有其中一部分頁面。每一個頁面的大小隻有 4K,一個 Redis 實例裏面通常都會有成千上萬的頁面。
子進程由於數據沒有變化,它能看到的內存裏的數據在進程產生的一瞬間就凝固了,不再會改變,這也是爲何 Redis 的持久化叫「快照」的緣由。接下來子進程就能夠很是安心的遍歷數據了進行序列化寫磁盤了。
AOF 日誌存儲的是 Redis 服務器的順序指令序列,AOF 日誌只記錄對內存進行修改的指令記錄。
假設 AOF 日誌記錄了自 Redis 實例建立以來全部的修改性指令序列,那麼就能夠經過對一個空的 Redis 實例順序執行全部的指令,也就是「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。
Redis 會在收到客戶端修改指令後,進行參數校驗進行邏輯處理後,若是沒問題,就當即將該指令文本存儲到 AOF 日誌中,也就是先執行指令纔將日誌存盤。這點不一樣於leveldb、hbase等存儲引擎,它們都是先存儲日誌再作邏輯處理。
Redis 在長期運行的過程當中,AOF 的日誌會越變越長。若是實例宕機重啓,重放整個 AOF 日誌會很是耗時,致使長時間 Redis 沒法對外提供服務。因此須要對 AOF 日誌瘦身。
Redis 提供了 bgrewriteaof 指令用於對 AOF 日誌進行瘦身。其原理就是開闢一個子進程對內存進行遍歷轉換成一系列 Redis 的操做指令,序列化到一個新的 AOF 日誌文件中。序列化完畢後再將操做期間發生的增量 AOF 日誌追加到這個新的 AOF 日誌文件中,追加完畢後就當即替代舊的 AOF 日誌文件了,瘦身工做就完成了。
BGREWRITEAOF命令,這個命令會經過移除AOF文件中的冗餘命令
來重寫(rewrite)AOF文件,使AOF文件的體積變得儘量地小。
AOF重寫:
(1) 隨着AOF文件愈來愈大,裏面會有大部分是重複命令或者能夠合併的命令(100次incr = set key 100)
(2) 重寫的好處:減小AOF日誌尺寸,減小內存佔用,加快數據庫恢復時間。
AOF 日誌是以文件的形式存在的,當程序對 AOF 日誌文件進行寫操做時,其實是將內容寫到了內核爲文件描述符分配的一個內存緩存中,而後內核會異步將髒數據刷回到磁盤的。
這就意味着若是機器忽然宕機,AOF 日誌內容可能尚未來得及徹底刷到磁盤中,這個時候就會出現日誌丟失。那該怎麼辦?
Linux 的glibc
提供了fsync(int fd)
函數能夠將指定文件的內容強制從內核緩存刷到磁盤。只要 Redis 進程實時調用 fsync 函數就能夠保證 aof 日誌不丟失。可是 fsync 是一個磁盤 IO 操做,它很慢!若是 Redis 執行一條指令就要 fsync 一次,那麼 Redis 高性能的地位就不保了。
因此在生產環境的服務器中,Redis 一般是每隔 1s 左右執行一次 fsync 操做,週期 1s 是能夠配置的。這是在數據安全性和性能之間作了一個折中,在保持高性能的同時,儘量使得數據少丟失。
Redis 一樣也提供了另外兩種策略,一個是永不 fsync——讓操做系統來決定什麼時候同步磁盤,很不安全,另外一個是來一個指令就 fsync 一次——很是慢。可是在生產環境基本不會使用,瞭解一下便可。
重啓 Redis 時,咱們不多使用 rdb 來恢復內存狀態,由於會丟失大量數據。咱們一般使用 AOF 日誌重放,可是重放 AOF 日誌性能相對 rdb 來講要慢不少,這樣在 Redis 實例很大的狀況下,啓動須要花費很長的時間。
Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內容和增量的 AOF 日誌文件存在一塊兒。這裏的 AOF 日誌再也不是全量的日誌,而是自持久化開始到持久化結束的這段時間發生的增量 AOF 日誌,一般這部分 AOF 日誌很小。
混合持久化的時候,aof 文件開頭是 rdb 的格式, 先加載 rdb 內容再加載剩餘的 aof。 打開AOF文件以後,首先讀取5個字符,若是是"REDIS",那麼就說明這是一個混合持久化的AOF文件。正確的RDB格式必定是以"REDIS"開頭,而純AOF格式則必定以"*"開頭的。
「REDIS」開頭,就會進入rdbLoadRio函數就以約定好的協議解析加載rdb數據,直至遇到RDB_OPCODE_EOF結束標記,返回loadAppendOnlyFile函數繼續以AOF格式解析文件,直到結束整個加載過程完成。
因而在 Redis 重啓的時候,能夠先加載 rdb 的內容,而後再重放增量 AOF 日誌就能夠徹底替代以前的 AOF 全量文件重放,重啓效率所以大幅獲得提高。