我們通常將 Redis 作為緩存使用酵幕,提高讀取響應(yīng)性能扰藕,一旦 Redis 宕機(jī),內(nèi)存中的數(shù)據(jù)全部丟失芳撒,假如現(xiàn)在直接訪問數(shù)據(jù)庫(kù)大量流量打到 MySQL 可能會(huì)帶來(lái)更加嚴(yán)重的問題邓深。
另外慢慢的從數(shù)據(jù)庫(kù)讀取放到 Redis 性能必然比不過(guò)從 Redis 獲取快,也會(huì)導(dǎo)致響應(yīng)變慢笔刹。
Redis 為了實(shí)現(xiàn)無(wú)畏宕機(jī)快速恢復(fù)芥备,設(shè)計(jì)了兩大殺手锏,分別是 AOF(Append Only FIle)日志和 RDB 快照舌菜。
RDB 內(nèi)存快照萌壳,讓宕機(jī)快速恢復(fù)
在 Redis 執(zhí)行「寫」指令過(guò)程中,內(nèi)存數(shù)據(jù)會(huì)一直變化日月。所謂的內(nèi)存快照袱瓮,指的就是 Redis 內(nèi)存中的數(shù)據(jù)在某一刻的狀態(tài)數(shù)據(jù)。
好比時(shí)間定格在某一刻爱咬,當(dāng)我們拍照的尺借,通過(guò)照片就能把某一刻的瞬間畫面完全記錄下來(lái)。
Redis 跟這個(gè)類似精拟,就是把某一刻的數(shù)據(jù)以文件的形式拍下來(lái)褐望,寫到磁盤上。這個(gè)快照文件叫做 RDB 文件串前,RDB 就是 Redis DataBase 的縮寫瘫里。
Redis 通過(guò)定時(shí)執(zhí)行 RDB 內(nèi)存快照,這樣就不必每次執(zhí)行「寫」指令都寫磁盤荡碾,只需要在執(zhí)行內(nèi)存快照的時(shí)候?qū)懘疟P谨读。既保證了唯快不破,還實(shí)現(xiàn)了持久化坛吁,宕機(jī)快速恢復(fù)劳殖。
在做數(shù)據(jù)恢復(fù)時(shí),直接將 RDB 文件讀入內(nèi)存完成恢復(fù)拨脉。
生成 RDB 策略
Redis 提供了兩個(gè)指令用于生成 RDB 文件:
- save:主線程執(zhí)行哆姻,會(huì)阻塞;
- bgsave:調(diào)用 glibc 的函數(shù)fork產(chǎn)生一個(gè)子進(jìn)程用于寫入 RDB 文件玫膀,快照持久化完全交給子進(jìn)程來(lái)處理矛缨,父進(jìn)程繼續(xù)處理客戶端請(qǐng)求,生成 RDB 文件的默認(rèn)配置
Redis 如何實(shí)現(xiàn)一邊處理寫請(qǐng)求,同時(shí)生成 RDB 文件呢箕昭?
Redis 使用操作系統(tǒng)的多進(jìn)程寫時(shí)復(fù)制技術(shù) COW(Copy On Write) 來(lái)實(shí)現(xiàn)快照持久化灵妨,這個(gè)機(jī)制很有意思,也很少人知道落竹。多進(jìn)程 COW 也是鑒定程序員知識(shí)廣度的一個(gè)重要指標(biāo)泌霍。
Redis 在持久化時(shí)會(huì)調(diào)用 glibc 的函數(shù)fork產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來(lái)處理述召,父進(jìn)程繼續(xù)處理客戶端請(qǐng)求朱转。
子進(jìn)程剛剛產(chǎn)生時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段积暖。這時(shí)你可以將父子進(jìn)程想像成一個(gè)連體嬰兒穆壕,共享身體戳粒。
bgsave 子進(jìn)程可以共享主線程的所有內(nèi)存數(shù)據(jù),讀取主線程的數(shù)據(jù)并寫入到 RDB 文件。
在執(zhí)行 SAVE 命令或者BGSAVE命令創(chuàng)建一個(gè)新的 RDB 文件時(shí)梗顺,程序會(huì)對(duì)數(shù)據(jù)庫(kù)中的鍵進(jìn)行檢查舆乔,已過(guò)期的鍵不會(huì)被保存到新創(chuàng)建的 RDB 文件中羽氮。
當(dāng)主線程執(zhí)行寫指令修改數(shù)據(jù)的時(shí)候杨蛋,這個(gè)數(shù)據(jù)就會(huì)復(fù)制一份副本, bgsave 子進(jìn)程讀取這個(gè)副本數(shù)據(jù)寫到 RDB 文件错览,所以主線程就可以直接修改原來(lái)的數(shù)據(jù)纫雁。
這既保證了快照的完整性,也允許主線程同時(shí)對(duì)數(shù)據(jù)進(jìn)行修改倾哺,避免了對(duì)正常業(yè)務(wù)的影響轧邪。
Redis 會(huì)使用 bgsave 對(duì)當(dāng)前內(nèi)存中的所有數(shù)據(jù)做快照,這個(gè)操作是子進(jìn)程在后臺(tái)完成的羞海,這就允許主線程同時(shí)可以修改數(shù)據(jù)忌愚。
RDB的優(yōu)缺點(diǎn)
快照的恢復(fù)速度快,但是生成 RDB 文件頻率不好把握却邓,頻率過(guò)低宕機(jī)丟失的數(shù)據(jù)就會(huì)比較多硕糊;太快,又會(huì)消耗額外開銷腊徙。
RDB 采用二進(jìn)制 + 數(shù)據(jù)壓縮的方式寫磁盤简十,文件體積小,數(shù)據(jù)恢復(fù)速度快撬腾。
AOF 寫后日志螟蝙,避免宕機(jī)數(shù)據(jù)丟失
AOF 日志存儲(chǔ)的是 Redis 服務(wù)器的順序指令序列,AOF 日志只記錄對(duì)內(nèi)存進(jìn)行修改的指令記錄民傻。
假設(shè) AOF 日志記錄了自 Redis 實(shí)例創(chuàng)建以來(lái)所有的修改性指令序列胰默,那么就可以通過(guò)對(duì)一個(gè)空的 Redis 實(shí)例順序執(zhí)行所有的指令场斑,也就是「重放」,來(lái)恢復(fù) Redis 當(dāng)前實(shí)例的內(nèi)存數(shù)據(jù)結(jié)構(gòu)的狀態(tài)初坠。
寫前與寫后日志對(duì)比
寫前日志(Write Ahead Log, WAL): 在實(shí)際寫數(shù)據(jù)之前和簸,將修改的數(shù)據(jù)寫到日志文件中彭雾,故障恢復(fù)得以保證碟刺。
比如 MySQL Innodb 存儲(chǔ)引擎 中的 redo log(重做日志)便是記錄修改的數(shù)據(jù)日志,在實(shí)際修改數(shù)據(jù)前先記錄修改日志在執(zhí)行修改數(shù)據(jù)薯酝。
寫后日志: 先執(zhí)行「寫」指令請(qǐng)求半沽,將數(shù)據(jù)寫入內(nèi)存,再記錄日志吴菠。
AOF 使用寫后日志這種方式者填。寫后日志避免了額外的檢查開銷,不需要對(duì)執(zhí)行的命令進(jìn)行語(yǔ)法檢查做葵。如果使用寫前日志的話占哟,就需要先檢查語(yǔ)法是否有誤,否則日志記錄了錯(cuò)誤的命令酿矢,在使用日志恢復(fù)的時(shí)候就會(huì)出錯(cuò)榨乎。
另外,寫后才記錄日志瘫筐,不會(huì)阻塞當(dāng)前的「寫」指令執(zhí)行蜜暑。
“有了 AOF 就萬(wàn)無(wú)一失了么?”
假如 Redis 剛執(zhí)行完指令策肝,還沒記錄日志宕機(jī)了肛捍,就有可能丟失這個(gè)命令相關(guān)的數(shù)據(jù)。
還有之众,AOF 避免了當(dāng)前命令的阻塞拙毫,但是可能會(huì)給下一個(gè)命令帶來(lái)阻塞的風(fēng)險(xiǎn)。AOF 日志是主線程執(zhí)行棺禾,將日志寫入磁盤過(guò)程中缀蹄,如果磁盤壓力大就會(huì)導(dǎo)致寫磁盤很慢,導(dǎo)致后續(xù)的「寫」指令阻塞帘睦。
這兩個(gè)問題與磁盤寫回有關(guān)袍患,如果能合理的控制「寫」指令執(zhí)行完后 AOF 日志寫回磁盤的時(shí)機(jī),問題就迎刃而解竣付。
寫回策略
為了提高文件的寫入效率诡延,當(dāng)用戶調(diào)用 write 函數(shù),將一些數(shù)據(jù)寫入到文件的時(shí)候古胆,操作系統(tǒng)通常會(huì)將寫入數(shù)據(jù)暫時(shí)保存在一個(gè)內(nèi)存緩沖區(qū)里面肆良,等到緩沖區(qū)的空間被填滿筛璧、或者超過(guò)了指定的時(shí)限之后,才真正地將緩沖區(qū)中的數(shù)據(jù)寫入到磁盤里面惹恃。
這種做法雖然提高了效率夭谤,但也為寫入數(shù)據(jù)帶來(lái)了安全問題,因?yàn)槿绻?jì)算機(jī)發(fā)生停機(jī)巫糙,那么保存在內(nèi)存緩沖區(qū)里面的寫入數(shù)據(jù)將會(huì)丟失朗儒。
為此,系統(tǒng)提供了fsync和fdatasync兩個(gè)同步函數(shù)参淹,它們可以強(qiáng)制讓操作系統(tǒng)立即將緩沖區(qū)中的數(shù)據(jù)寫入到硬盤里面醉锄,從而確保寫入數(shù)據(jù)的安全性。
Redis 提供的 AOF 配置項(xiàng)appendfsync寫回策略直接決定 AOF 持久化功能的效率和安全性浙值。
- always:同步寫回恳不,寫指令執(zhí)行完畢立馬將 aof_buf緩沖區(qū)中的內(nèi)容刷寫到 AOF 文件。
- everysec:每秒寫回开呐,寫指令執(zhí)行完烟勋,日志只會(huì)寫到 AOF 文件緩沖區(qū),每隔一秒就把緩沖區(qū)內(nèi)容同步到磁盤筐付。
- no: 操作系統(tǒng)控制卵惦,寫執(zhí)行執(zhí)行完畢,把日志寫到 AOF 文件內(nèi)存緩沖區(qū)家妆,由操作系統(tǒng)決定何時(shí)刷寫到磁盤鸵荠。
沒有兩全其美的策略,我們需要在性能和可靠性上做一個(gè)取舍伤极。
always同步寫回可以做到數(shù)據(jù)不丟失蛹找,但是每個(gè)「寫」指令都需要寫入磁盤,性能最差哨坪。
everysec每秒寫回庸疾,避免了同步寫回的性能開銷,發(fā)生宕機(jī)可能有一秒位寫入磁盤的數(shù)據(jù)丟失当编,在性能和可靠性之間做了折中届慈。
no操作系統(tǒng)控制,執(zhí)行寫指令后就寫入 AOF 文件緩沖就可以執(zhí)行后續(xù)的「寫」指令忿偷,性能最好金顿,但是有可能丟失很多的數(shù)據(jù)。
日志過(guò)大:AOF 重寫機(jī)制
Redis 設(shè)計(jì)了一個(gè)殺手锏「AOF 重寫機(jī)制」鲤桥,Redis 提供了 bgrewriteaof指令用于對(duì) AOF 日志進(jìn)行瘦身揍拆。
其原理就是開辟一個(gè)子進(jìn)程對(duì)內(nèi)存進(jìn)行遍歷轉(zhuǎn)換成一系列 Redis 的操作指令,序列化到一個(gè)新的 AOF 日志文件中茶凳。序列化完畢后再將操作期間發(fā)生的增量 AOF 日志追加到這個(gè)新的 AOF 日志文件中嫂拴,追加完畢后就立即替代舊的 AOF 日志文件了播揪,瘦身工作就完成了。
重寫機(jī)制有「多變一」功能筒狠,將舊日志中的多條指令猪狈,在重寫后就變成了一條指令。
重寫過(guò)程
和 AOF 日志由主線程寫回不同辩恼,重寫過(guò)程是由后臺(tái)子進(jìn)程 bgrewriteaof 來(lái)完成的雇庙,這也是為了避免阻塞主線程,導(dǎo)致數(shù)據(jù)庫(kù)性能下降运挫。
“AOF 重寫也有一個(gè)重寫日志状共,為什么它不共享使用 AOF 本身的日志呢套耕?”
1谁帕、一個(gè)原因是父子進(jìn)程寫同一個(gè)文件必然會(huì)產(chǎn)生競(jìng)爭(zhēng)問題,控制競(jìng)爭(zhēng)就意味著會(huì)影響父進(jìn)程的性能冯袍。
2匈挖、如果 AOF 重寫過(guò)程中失敗了,那么原本的 AOF 文件相當(dāng)于被污染了康愤,無(wú)法做恢復(fù)使用儡循。所以 Redis AOF 重寫一個(gè)新文件,重寫失敗的話征冷,直接刪除這個(gè)文件就好了择膝,不會(huì)對(duì)原先的 AOF 文件產(chǎn)生影響。等重寫完成之后检激,直接替換舊文件即可肴捉。
Redis 4.0 混合日志模型
Redis 4.0 為了解決這個(gè)問題,帶來(lái)了一個(gè)新的持久化選項(xiàng)——混合持久化叔收。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起齿穗。這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結(jié)束的這段時(shí)間發(fā)生的增量 AOF 日志饺律,通常這部分 AOF 日志很小窃页。
于是在 Redis 重啟的時(shí)候,可以先加載 rdb 的內(nèi)容复濒,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放脖卖,重啟效率因此大幅得到提升。
所以 RDB 內(nèi)存快照以稍微慢一點(diǎn)的頻率執(zhí)行巧颈,在兩次 RDB 快照期間使用 AOF 日志記錄期間發(fā)生的所有「寫」操作畦木。
這樣快照就不用頻繁的執(zhí)行,同時(shí)由于 AOF 只需要記錄兩次快照之間發(fā)生的「寫」指令洛二,不需要記錄所有的操作馋劈,避免出現(xiàn)文件過(guò)大的情況攻锰。