一文了解Redis持久化
一、持久化簡介
Redis 的數(shù)據(jù) 全部存儲 在 內(nèi)存 中,如果 突然宕機,數(shù)據(jù)就會全部丟失,因此必須有一套機制來保證 Redis 的數(shù)據(jù)不會因為故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內(nèi)存中的數(shù)據(jù)庫狀態(tài) 保存到磁盤 中。
持久化發(fā)生了什么 | 從內(nèi)存到磁盤
我們來稍微考慮一下 Redis 作為一個 "內(nèi)存數(shù)據(jù)庫" 要做的關(guān)于持久化的事情。通常來說,從客戶端發(fā)起請求開始,到服務(wù)器真實地寫入磁盤,需要發(fā)生如下幾件事情:
詳細版 的文字描述大概就是下面這樣:
客戶端向數(shù)據(jù)庫 發(fā)送寫命令 (數(shù)據(jù)在客戶端的內(nèi)存中)
數(shù)據(jù)庫 接收 到客戶端的 寫請求 (數(shù)據(jù)在服務(wù)器的內(nèi)存中)
數(shù)據(jù)庫 調(diào)用系統(tǒng) API 將數(shù)據(jù)寫入磁盤 (數(shù)據(jù)在內(nèi)核緩沖區(qū)中)
操作系統(tǒng)將 寫緩沖區(qū) 傳輸?shù)?磁盤控控制器 (數(shù)據(jù)在磁盤緩存中)
操作系統(tǒng)的磁盤控制器將數(shù)據(jù) 寫入實際的物理媒介 中 (數(shù)據(jù)在磁盤中)
注意: 上面的過程其實是 極度精簡 的,在實際的操作系統(tǒng)中,緩存 和 緩沖區(qū) 會比這 多得多...
如何盡可能保證持久化的安全
如果我們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 并且沒有接觸到內(nèi)核,那么在 上述步驟 3 成功返回之后,我們就認為成功了。即使進程崩潰,操作系統(tǒng)仍然會幫助我們把數(shù)據(jù)正確地寫入磁盤。
如果我們考慮 停電/ 火災(zāi) 等 更具災(zāi)難性 的事情,那么只有在完成了第 5 步之后,才是安全的。
機房”火了“
所以我們可以總結(jié)得出數(shù)據(jù)安全最重要的階段是:步驟三、四、五,即:
數(shù)據(jù)庫軟件調(diào)用寫操作將用戶空間的緩沖區(qū)轉(zhuǎn)移到內(nèi)核緩沖區(qū)的頻率是多少?
內(nèi)核多久從緩沖區(qū)取數(shù)據(jù)刷新到磁盤控制器?
磁盤控制器多久把數(shù)據(jù)寫入物理媒介一次?
注意: 如果真的發(fā)生災(zāi)難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丟失,所以只能 盡可能地保證 數(shù)據(jù)的安全,這對于所有數(shù)據(jù)庫來說都是一樣的。
我們從 第三步 開始。Linux 系統(tǒng)提供了清晰、易用的用于操作文件的 POSIX file API,20 多年過去,仍然還有很多人對于這一套 API 的設(shè)計津津樂道,我想其中一個原因就是因為你光從 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 設(shè)計最佳實踐的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html
所以,我們有很好的可用的 API 來完成 第三步,但是對于成功返回之前,我們對系統(tǒng)調(diào)用花費的時間沒有太多的控制權(quán)。
然后我們來說說 第四步。我們知道,除了早期對電腦特別了解那幫人 (操作系統(tǒng)就這幫人搞的),實際的物理硬件都不是我們能夠 直接操作 的,都是通過 操作系統(tǒng)調(diào)用 來達到目的的。為了防止過慢的 I/O 操作拖慢整個系統(tǒng)的運行,操作系統(tǒng)層面做了很多的努力,譬如說 上述第四步 提到的 寫緩沖區(qū),并不是所有的寫操作都會被立即寫入磁盤,而是要先經(jīng)過一個緩沖區(qū),默認情況下,Linux 將在 30 秒 后實際提交寫入。
但是很明顯,30 秒 并不是 Redis 能夠承受的,這意味著,如果發(fā)生故障,那么最近 30 秒內(nèi)寫入的所有數(shù)據(jù)都可能會丟失。幸好 PROSIX API 提供了另一個解決方案:fsync,該命令會 強制 內(nèi)核將 緩沖區(qū) 寫入 磁盤,但這是一個非常消耗性能的操作,每次調(diào)用都會 阻塞等待 直到設(shè)備報告 IO 完成,所以一般在生產(chǎn)環(huán)境的服務(wù)器中,Redis 通常是每隔 1s 左右執(zhí)行一次 fsync 操作。
到目前為止,我們了解到了如何控制 第三步 和 第四步,但是對于 第五步,我們 完全無法控制。也許一些內(nèi)核實現(xiàn)將試圖告訴驅(qū)動實際提交物理介質(zhì)上的數(shù)據(jù),或者控制器可能會為了提高速度而重新排序?qū)懖僮?#xff0c;不會盡快將數(shù)據(jù)真正寫到磁盤上,而是會等待幾個多毫秒。這完全是我們無法控制的。
二、Redis 中的兩種持久化方式
方式一:快照
Redis 快照 是最簡單的 Redis 持久性模式。當滿足特定條件時,它將生成數(shù)據(jù)集的時間點快照,例如,如果先前的快照是在2分鐘前創(chuàng)建的,并且現(xiàn)在已經(jīng)至少有 100 次新寫入,則將創(chuàng)建一個新的快照。此條件可以由用戶配置 Redis 實例來控制,也可以在運行時修改而無需重新啟動服務(wù)器。快照作為包含整個數(shù)據(jù)集的單個 .rdb 文件生成。
但我們知道,Redis 是一個 單線程 的程序,這意味著,我們不僅僅要響應(yīng)用戶的請求,還需要進行內(nèi)存快照。而后者要求 Redis 必須進行 IO 操作,這會嚴重拖累服務(wù)器的性能。
還有一個重要的問題是,我們在 持久化的同時,內(nèi)存數(shù)據(jù)結(jié)構(gòu) 還可能在 變化,比如一個大型的 hash 字典正在持久化,結(jié)果一個請求過來把它刪除了,可是這才剛持久化結(jié)束,咋辦?
使用系統(tǒng)多進程 COW(Copy On Write) 機制 | fork 函數(shù)
操作系統(tǒng)多進程 COW(Copy On Write) 機制 拯救了我們。Redis 在持久化時會調(diào)用 glibc 的函數(shù) fork 產(chǎn)生一個子進程,簡單理解也就是基于當前進程 復制 了一個進程,主進程和子進程會共享內(nèi)存里面的代碼塊和數(shù)據(jù)段:
這里多說一點,為什么 fork 成功調(diào)用后會有兩個返回值呢? 因為子進程在復制時復制了父進程的堆棧段,所以兩個進程都停留在了 fork 函數(shù)中 (都在同一個地方往下繼續(xù)"同時"執(zhí)行),等待返回,所以 一次在父進程中返回子進程的 pid,另一次在子進程中返回零,系統(tǒng)資源不夠時返回負數(shù)。(偽代碼如下)
pid = os.fork() if pid > 0:handle_client_request() # 父進程繼續(xù)處理客戶端請求 if pid == 0:handle_snapshot_write() # 子進程處理快照寫磁盤 if pid < 0:# fork error所以 快照持久化 可以完全交給 子進程 來處理,父進程 則繼續(xù) 處理客戶端請求。子進程 做數(shù)據(jù)持久化,它 不會修改現(xiàn)有的內(nèi)存數(shù)據(jù)結(jié)構(gòu),它只是對數(shù)據(jù)結(jié)構(gòu)進行遍歷讀取,然后序列化寫到磁盤中。但是 父進程 不一樣,它必須持續(xù)服務(wù)客戶端請求,然后對 內(nèi)存數(shù)據(jù)結(jié)構(gòu)進行不間斷的修改。
這個時候就會使用操作系統(tǒng)的 COW 機制來進行 數(shù)據(jù)段頁面 的分離。數(shù)據(jù)段是由很多操作系統(tǒng)的頁面組合而成,當父進程對其中一個頁面的數(shù)據(jù)進行修改時,會將被共享的頁面復 制一份分離出來,然后 對這個復制的頁面進行修改。這時 子進程 相應(yīng)的頁面是 沒有變化的,還是進程產(chǎn)生時那一瞬間的數(shù)據(jù)。
子進程因為數(shù)據(jù)沒有變化,它能看到的內(nèi)存里的數(shù)據(jù)在進程產(chǎn)生的一瞬間就凝固了,再也不會改變,這也是為什么 Redis 的持久化 叫「快照」的原因。接下來子進程就可以非常安心的遍歷數(shù)據(jù)了進行序列化寫磁盤了。
方式二:AOF
快照不是很持久。如果運行 Redis 的計算機停止運行,電源線出現(xiàn)故障或者您 kill -9 的實例意外發(fā)生,則寫入 Redis 的最新數(shù)據(jù)將丟失。盡管這對于某些應(yīng)用程序可能不是什么大問題,但有些使用案例具有充分的耐用性,在這些情況下,快照并不是可行的選擇。
AOF(Append Only File - 僅追加文件) 它的工作方式非常簡單:每次執(zhí)行 修改內(nèi)存 中數(shù)據(jù)集的寫操作時,都會 記錄 該操作。假設(shè) AOF 日志記錄了自 Redis 實例創(chuàng)建以來 所有的修改性指令序列,那么就可以通過對一個空的 Redis 實例 順序執(zhí)行所有的指令,也就是 「重放」,來恢復 Redis 當前實例的內(nèi)存數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。
為了展示 AOF 在實際中的工作方式,我們來做一個簡單的實驗:
./redis-server --appendonly yes # 設(shè)置一個新實例為 AOF 模式然后我們執(zhí)行一些寫操作:
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前三個操作實際上修改了數(shù)據(jù)集,第四個操作沒有修改,因為沒有指定名稱的鍵。這是 AOF 日志保存的文本:
$ cat appendonly.aof *2 $6 SELECT $1 0 *3 $3 set $4 key1 $5 Hello *3 $6 append $4 key1 $7World! *2 $3 del $4 key1如您所見,最后的那一條 DEL 指令不見了,因為它沒有對數(shù)據(jù)集進行任何修改。
就是這么簡單。當 Redis 收到客戶端修改指令后,會先進行參數(shù)校驗、邏輯處理,如果沒問題,就 立即 將該指令文本 存儲 到 AOF 日志中,也就是說,先執(zhí)行指令再將日志存盤。這一點不同于 MySQL、LevelDB、HBase 等存儲引擎,如果我們先存儲日志再做邏輯處理,這樣就可以保證即使宕機了,我們?nèi)匀豢梢酝ㄟ^之前保存的日志恢復到之前的數(shù)據(jù)狀態(tài),但是 Redis 為什么沒有這么做呢?
Emmm... 沒找到特別滿意的答案,引用一條來自知乎上的回答吧:
@緣于專注 - 我甚至覺得沒有什么特別的原因。僅僅是因為,由于AOF文件會比較大,為了避免寫入無效指令(錯誤指令),必須先做指令檢查?如何檢查,只能先執(zhí)行了。因為語法級別檢查并不能保證指令的有效性,比如刪除一個不存在的key。而MySQL這種是因為它本身就維護了所有的表的信息,所以可以語法檢查后過濾掉大部分無效指令直接記錄日志,然后再執(zhí)行。
更多討論參見:為什么Redis先執(zhí)行指令,再記錄AOF日志,而不是像其它存儲引擎一樣反過來呢?- https://www.zhihu.com/question/342427472
AOF 重寫
Redis 在長期運行的過程中,AOF 的日志會越變越長。如果實例宕機重啟,重放整個 AOF 日志會非常耗時,導致長時間 Redis 無法對外提供服務(wù)。所以需要對 AOF 日志 "瘦身"。
Redis 提供了 bgrewriteaof 指令用于對 AOF 日志進行瘦身。其 原理 就是 開辟一個子進程 對內(nèi)存進行 遍歷 轉(zhuǎn)換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日志文件 中。序列化完畢后再將操作期間發(fā)生的 增量 AOF 日志 追加到這個新的 AOF 日志文件中,追加完畢后就立即替代舊的 AOF 日志文件了,瘦身工作就完成了。
fsync
AOF 日志是以文件的形式存在的,當程序?qū)?AOF 日志文件進行寫操作時,實際上是將內(nèi)容寫到了內(nèi)核為文件描述符分配的一個內(nèi)存緩存中,然后內(nèi)核會異步將臟數(shù)據(jù)刷回到磁盤的。
就像我們 上方第四步 描述的那樣,我們需要借助 glibc 提供的 fsync(int fd) 函數(shù)來講指定的文件內(nèi)容 強制從內(nèi)核緩存刷到磁盤。但 "強制開車" 仍然是一個很消耗資源的一個過程,需要 "節(jié)制"!通常來說,生產(chǎn)環(huán)境的服務(wù)器,Redis 每隔 1s 左右執(zhí)行一次 fsync 操作就可以了。
Redis 同樣也提供了另外兩種策略,一個是 永不 fsync,來讓操作系統(tǒng)來決定合適同步磁盤,很不安全,另一個是 來一個指令就 fsync 一次,非常慢。但是在生產(chǎn)環(huán)境基本不會使用,了解一下即可。
Redis 4.0 混合持久化
重啟 Redis 時,我們很少使用 rdb 來恢復內(nèi)存狀態(tài),因為會丟失大量數(shù)據(jù)。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 rdb 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。
Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是 自持久化開始到持久化結(jié)束 的這段時間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小:
于是在 Redis 重啟的時候,可以先加載 rdb 的內(nèi)容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。
擴展閱讀
Redis 數(shù)據(jù)備份與恢復 | 菜鳥教程 - https://www.runoob.com/redis/redis-backup.html
Java Fork/Join 框架 - https://www.cnblogs.com/cjsblog/p/9078341.html
參考資料
Redis persistence demystified | antirez weblog (作者博客) - http://oldblog.antirez.com/post/redis-persistence-demystified.html
操作系統(tǒng) — fork()函數(shù)的使用與底層原理 - https://blog.csdn.net/Dawn_sf/article/details/78709839
磁盤和內(nèi)存讀寫簡單原理 - https://blog.csdn.net/zhanghongzheng3213/article/details/54141202
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的一文了解Redis持久化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 二叉树重建
- 下一篇: NYOJ 330 一个简单的数学题