Redis 的數據 所有存儲 在 內存 中,若是 忽然宕機,數據就會所有丟失,所以必須有一套機制來保證 Redis 的數據不會由於故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。html
咱們來稍微考慮一下 Redis 做爲一個 "內存數據庫" 要作的關於持久化的事情。一般來講,從客戶端發起請求開始,到服務器真實地寫入磁盤,須要發生以下幾件事情:python
詳細版 的文字描述大概就是下面這樣:git
注意: 上面的過程實際上是 極度精簡 的,在實際的操做系統中,緩存 和 緩衝區 會比這 多得多...程序員
若是咱們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 而且沒有接觸到內核,那麼在 上述步驟 3 成功返回以後,咱們就認爲成功了。即便進程崩潰,操做系統仍然會幫助咱們把數據正確地寫入磁盤。github
若是咱們考慮 停電/ 火災 等 更具災難性 的事情,那麼只有在完成了第 5 步以後,纔是安全的。web
因此咱們能夠總結得出數據安全最重要的階段是:步驟3、4、五,即:redis
咱們從 第三步 開始。Linux 系統提供了清晰、易用的用於操做文件的 POSIX file API
,20
多年過去,仍然還有不少人對於這一套 API
的設計津津樂道,我想其中一個緣由就是由於你光從 API
的命名就可以很清晰地知道這一套 API 的用途:數據庫
int open(const char *path, int oflag, .../*,mode_t mode */); int close (int filedes);int remove( const char *fname ); ssize_t write(int fildes, const void *buf, size_t nbyte); ssize_t read(int fildes, void *buf, size_t nbyte);
因此,咱們有很好的可用的 API
來完成 第三步,可是對於成功返回以前,咱們對系統調用花費的時間沒有太多的控制權。緩存
而後咱們來講說 第四步。咱們知道,除了早期對電腦特別瞭解那幫人 (操做系統就這幫人搞的),實際的物理硬件都不是咱們可以 直接操做 的,都是經過 操做系統調用 來達到目的的。爲了防止過慢的 I/O 操做拖慢整個系統的運行,操做系統層面作了不少的努力,譬如說 上述第四步 提到的 寫緩衝區,並非全部的寫操做都會被當即寫入磁盤,而是要先通過一個緩衝區,默認狀況下,Linux 將在 30 秒 後實際提交寫入。安全
可是很明顯,30 秒 並非 Redis 可以承受的,這意味着,若是發生故障,那麼最近 30 秒內寫入的全部數據均可能會丟失。幸虧 PROSIX API
提供了另外一個解決方案:fsync
,該命令會 強制 內核將 緩衝區 寫入 磁盤,但這是一個很是消耗性能的操做,每次調用都會 阻塞等待 直到設備報告 IO 完成,因此通常在生產環境的服務器中,Redis 一般是每隔 1s 左右執行一次 fsync
操做。
到目前爲止,咱們瞭解到瞭如何控制 第三步
和 第四步
,可是對於 第五步,咱們 徹底沒法控制。也許一些內核實現將試圖告訴驅動實際提交物理介質上的數據,或者控制器可能會爲了提升速度而從新排序寫操做,不會盡快將數據真正寫到磁盤上,而是會等待幾個多毫秒。這徹底是咱們沒法控制的。
Redis 快照 是最簡單的 Redis 持久性模式。當知足特定條件時,它將生成數據集的時間點快照,例如,若是先前的快照是在2分鐘前建立的,而且如今已經至少有 100 次新寫入,則將建立一個新的快照。此條件能夠由用戶配置 Redis 實例來控制,也能夠在運行時修改而無需從新啓動服務器。快照做爲包含整個數據集的單個 .rdb
文件生成。
但咱們知道,Redis 是一個 單線程 的程序,這意味着,咱們不只僅要響應用戶的請求,還須要進行內存快照。然後者要求 Redis 必須進行 IO 操做,這會嚴重拖累服務器的性能。
還有一個重要的問題是,咱們在 持久化的同時,內存數據結構 還可能在 變化,好比一個大型的 hash 字典正在持久化,結果一個請求過來把它刪除了,但是這纔剛持久化結束,咋辦?
操做系統多進程 COW(Copy On Write) 機制 拯救了咱們。Redis 在持久化時會調用 glibc
的函數 fork
產生一個子進程,簡單理解也就是基於當前進程 複製 了一個進程,主進程和子進程會共享內存裏面的代碼塊和數據段:
這裏多說一點,爲何 fork 成功調用後會有兩個返回值呢? 由於子進程在複製時複製了父進程的堆棧段,因此兩個進程都停留在了 fork
函數中 (都在同一個地方往下繼續"同時"執行),等待返回,因此 一次在父進程中返回子進程的 pid,另外一次在子進程中返回零,系統資源不夠時返回負數。 (僞代碼以下)
pid = os.fork() if pid > 0: handle_client_request() # 父進程繼續處理客戶端請求 if pid == 0: handle_snapshot_write() # 子進程處理快照寫磁盤 if pid < 0: # fork error
因此 快照持久化 能夠徹底交給 子進程 來處理,父進程 則繼續 處理客戶端請求。子進程 作數據持久化,它 不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀取,而後序列化寫到磁盤中。可是 父進程 不同,它必須持續服務客戶端請求,而後對 內存數據結構進行不間斷的修改。
這個時候就會使用操做系統的 COW 機制來進行 數據段頁面 的分離。數據段是由不少操做系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面復
制一份分離出來,而後 對這個複製的頁面進行修改。這時 子進程 相應的頁面是 沒有變化的,仍是進程產生時那一瞬間的數據。
子進程由於數據沒有變化,它能看到的內存裏的數據在進程產生的一瞬間就凝固了,不再會改變,這也是爲何 Redis 的持久化 叫「快照」的緣由。接下來子進程就能夠很是安心的遍歷數據了進行序列化寫磁盤了。
快照不是很持久。若是運行 Redis 的計算機中止運行,電源線出現故障或者您 kill -9
的實例意外發生,則寫入 Redis 的最新數據將丟失。儘管這對於某些應用程序可能不是什麼大問題,但有些使用案例具備充分的耐用性,在這些狀況下,快照並非可行的選擇。
AOF(Append Only File - 僅追加文件) 它的工做方式很是簡單:每次執行 修改內存 中數據集的寫操做時,都會 記錄 該操做。假設 AOF 日誌記錄了自 Redis 實例建立以來 全部的修改性指令序列,那麼就能夠經過對一個空的 Redis 實例 順序執行全部的指令,也就是 「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。
爲了展現 AOF 在實際中的工做方式,咱們來作一個簡單的實驗:
./redis-server --appendonly yes # 設置一個新實例爲 AOF 模式
而後咱們執行一些寫操做:
redis 127.0.0.1:6379> set key1 Hello OK redis 127.0.0.1:6379> append key1 " World!" (integer) 12 redis 127.0.0.1:6379> del key1 (integer) 1 redis 127.0.0.1:6379> del non_existing_key (integer) 0
前三個操做實際上修改了數據集,第四個操做沒有修改,由於沒有指定名稱的鍵。這是 AOF 日誌保存的文本:
$ cat appendonly.aof *2 $6 SELECT $1 0 *3 $3 set $4 key1 $5 Hello *3 $6 append $4 key1 $7 World! *2 $3 del $4 key1
如您所見,最後的那一條 DEL
指令不見了,由於它沒有對數據集進行任何修改。
就是這麼簡單。當 Redis 收到客戶端修改指令後,會先進行參數校驗、邏輯處理,若是沒問題,就 當即 將該指令文本 存儲 到 AOF 日誌中,也就是說,先執行指令再將日誌存盤。這一點不一樣於 MySQL
、LevelDB
、HBase
等存儲引擎,若是咱們先存儲日誌再作邏輯處理,這樣就能夠保證即便宕機了,咱們仍然能夠經過以前保存的日誌恢復到以前的數據狀態,可是 Redis 爲何沒有這麼作呢?
Emmm... 沒找到特別滿意的答案,引用一條來自知乎上的回答吧:
- @緣於專一 - 我甚至以爲沒有什麼特別的緣由。僅僅是由於,因爲AOF文件會比較大,爲了不寫入無效指令(錯誤指令),必須先作指令檢查?如何檢查,只能先執行了。由於語法級別檢查並不能保證指令的有效性,好比刪除一個不存在的key。而MySQL這種是由於它自己就維護了全部的表的信息,因此能夠語法檢查後過濾掉大部分無效指令直接記錄日誌,而後再執行。
- 更多討論參見:爲何Redis先執行指令,再記錄AOF日誌,而不是像其它存儲引擎同樣反過來呢? - https://www.zhihu.com/question/342427472
Redis 在長期運行的過程當中,AOF 的日誌會越變越長。若是實例宕機重啓,重放整個 AOF 日誌會很是耗時,致使長時間 Redis 沒法對外提供服務。因此須要對 AOF 日誌 "瘦身"。
Redis 提供了 bgrewriteaof
指令用於對 AOF 日誌進行瘦身。其 原理 就是 開闢一個子進程 對內存進行 遍歷 轉換成一系列 Redis 的操做指令,序列化到一個新的 AOF 日誌文件 中。序列化完畢後再將操做期間發生的 增量 AOF 日誌 追加到這個新的 AOF 日誌文件中,追加完畢後就當即替代舊的 AOF 日誌文件了,瘦身工做就完成了。
AOF 日誌是以文件的形式存在的,當程序對 AOF 日誌文件進行寫操做時,其實是將內容寫到了內核爲文件描述符分配的一個內存緩存中,而後內核會異步將髒數據刷回到磁盤的。
就像咱們 上方第四步 描述的那樣,咱們須要藉助 glibc
提供的 fsync(int fd)
函數來說指定的文件內容 強制從內核緩存刷到磁盤。但 "強制開車" 仍然是一個很消耗資源的一個過程,須要 "節制"!一般來講,生產環境的服務器,Redis 每隔 1s 左右執行一次 fsync
操做就能夠了。
Redis 一樣也提供了另外兩種策略,一個是 永不 fsync
,來讓操做系統來決定合適同步磁盤,很不安全,另外一個是 來一個指令就 fsync
一次,很是慢。可是在生產環境基本不會使用,瞭解一下便可。
重啓 Redis 時,咱們不多使用 rdb
來恢復內存狀態,由於會丟失大量數據。咱們一般使用 AOF 日誌重放,可是重放 AOF 日誌性能相對 rdb
來講要慢不少,這樣在 Redis 實例很大的狀況下,啓動須要花費很長的時間。
Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb
文件的內容和增量的 AOF 日誌文件存在一塊兒。這裏的 AOF 日誌再也不是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,一般這部分 AOF 日誌很小:
因而在 Redis 重啓的時候,能夠先加載 rdb
的內容,而後再重放增量 AOF 日誌就能夠徹底替代以前的 AOF 全量文件重放,重啓效率所以大幅獲得提高。
- 本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
- 我的公衆號 :wmyskxz,我的獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!
很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!