緩存數據丟了,原來是Redis持久化沒玩明白
我們都知道Redis是微服務架構中重要的基礎數據庫中間件,通過Redis可以將數據庫中的數據緩存到內存中,當服務端有數據查詢請求的時候,可以直接從內存中獲取數據。如此,一方面服務端可以獲得比較快的數據請求響應,另一方面降低了后端關系數據庫的業務請求壓力。但是正所謂尺有所短,寸有所長,Redis最大的優勢就是內存數據也是最大的劣勢,因為一旦服務器宕機或者服務器重啟,內存中緩存的數據也會丟失。針對這樣的場景,Redis提供了三種數據持久化機制,分別是AOF、RDB以及混合持久化來應對這種異常情況。本文主要從Redis實現持久化遇到的問題出發,站在設計者的角度思考相關問題的解決思路。
?
AOF持久化
AOF持久化方式,即Append Only File,Redis通過記錄執行修改操作命令這種記小本本的方式進行內存數據持久化。當需要通過AOF日志進行恢復數據時,Redis服務端啟動后可以從日志文件中回放執行命令來實現內存數據恢復。當然了,AOF日志中記錄的都是修改的命令,查詢命令不會修改數據所以不需要進行記錄。
可能大家都比較熟悉WAL(Write Ahead Log),即日志預寫機制,它是數據庫非常常用的確保數據操作原子性以及持久性的技術手段。拿Mysql舉栗子,Mysql的WAL體現在undo log以及redo log等這些日志文件中,數據庫在執行修改操作的時候并不是立刻將數據更新到磁盤上,而是先記錄在日志中,主要目的是如果出現異常,可以直接從redo log中進行數據恢復,也就是說讓Mysql知道上次意外發生的時候操作到底有沒有成功,另外還可以將Mysql的隨機寫轉換為順序寫,提升IO性能。但是AOF卻不同,它是在Redis將數據寫入內存之后,再將相關的操作命令寫入AOF文件中。
那么問題來了,為什么Redis要采取這種獨特的數據記錄方式,而不是業界常用的WAL的方式呢?其實可以從以下兩個層面思考原因。
(1)AOF文件中保存了執行緩存的命令,以便于保證在需要恢復數據的時候可以進行命令重放恢復數據,因此需要保證執行命令的合法性,而通過先緩存數據再進行命令追加日志的方式可以確保追加到AOF文件中的的命令都是合法有效的,redis在恢復數據的時候不需要再去檢查命令是否有效,進一步提升內存數據恢復的效率。
(2)另外由于是在修改操作命令之后進行日志記錄,日志記錄的時候需要進行磁盤IO操作,因此不會阻塞當前的修改命令。
AOF文件內容是什么?
在搞清楚Redis為什么采用AOF文件記錄修改命令之后,我們再來看看AOF文件中到底包含了哪些內容。
Redis客戶端與服務端之間采用RESP協議進行通信,它是一種應用層協議,對于Redis這種以效率為追求目標的中間件,通信協議必定要簡單高效。就上面一條緩存操作命令來說:set mufeng handsome 對應的RESP報文就是*3$3set$6mufeng$8handsome,為了方便查看進行了手動換行。
我們來拆解下報文中各個屬性的含義,“*3”代表本次操作命令將由三個分布組成,每一部分都是通過"$數字"的形式作為起始,后面為對應的命令、鍵或者值。如此處的"$6"就表示后面的命令是一個6個字節的鍵值。所以,appenonly.aof文件中實際保存的就是這種格式的內容。
AOF有沒有丟數據的風險?
上文說到Redis通過AOF文件實現內存數據持久化,那么是不是就代表緩存數據保存就萬無一失了?這樣的持久化方式還有沒有數據丟失的風險呢?大家可以設想一下假設在操作完Redis之后,還沒來得及將命令寫入AOF文件就宕機了,那么這個操作命令就會丟失,對應的緩存數據最新值也會丟失。因為即便宕機異?;謴椭?,也沒辦法從AOF文件中執行丟失的操作命令了。因此,寫入AOF緩沖區的數據什么時候進行持久化落盤,直接決定著AOF持久化方式緩存數據丟失的風險大小。
三種AOF落盤策略
針對AOF緩存中的數據在什么時機寫入磁盤,Redis提供了三種AOF日志寫入策略供用戶進行選擇,通過后臺線程執行不同時機的AOF文件數據同步操作,在redis.conf配置文件中的配置項appendfsync可以進行配置。
【appendfsync:no】?
Redis不用管AOF緩沖區的數據什么時候寫入磁盤,將AOF緩沖區同步數據的操作權交給操作系統,操作系統決定什么時候將緩沖區的數據寫入磁盤中。
【appendfsync:everysec】
當Redis將數據寫入AOF緩沖區后,每隔1s將緩沖區的數據進行磁盤寫入。
【appendfsync:always】?
每執行一個修改命令,都需要將修改的命令進行落盤操作。
雖然Redis提供了這三種AOF日志落盤策略供用戶進行選擇,但是這三種策略實際上各有優缺點。
【appendfsync:no】
如果設置了由操作系統進行AOF緩沖區數據寫入,那么就相當于寫數據的時機完全交由操作系統來決定,此時redis對于緩沖區數據并不可以控制。
【appendfsync:everysec】
如果設置成每隔一秒進行緩存數據寫入,雖然不會像同步寫入那樣存在一定的性能消耗,但是由于存在一秒的時間間隔,如果在此期間出現服務器宕機,那么就會損失這一秒的緩存數據。
【appendfsync:always】
雖然可以基本實現數據不丟失,但是由于每次進行內存數據修改都要進行落盤操作,因此在一定程度上會影響主線程性能。
具體采取怎樣的配置策略還是要根據實際的業務場景來決定,一般推薦使用第二種配置策略【appendfsync:everysec】,在可靠性以及性能方面相對平衡一點。
AOF文件會越來越大嗎?
在了解了AOF日志磁盤寫入時機之后,我們繼續來思考下一個問題。無論采取什么樣的同步數據策略,最終都是要將修改命令寫入AOF文件中,因此隨著時間的推移,這個文件必定會越來越大。那么如果文件變得很大之后,無論是文件數據新寫入還是Redis通過AOF文件進行數據恢復,大文件的操作都會造成IO性能損耗。假如你是Redis的設計者,如果遇到這種情況你會怎么進行設計優化呢?我想無非有兩個優化思路,一個是化整為零,一個是想辦法縮小大文件。
化整為零
當單個文件過大時,我們很容易想到的優化方法就是將這個大文件拆分為若干個小文件。這就好比系統中一旦出現過千萬數據庫表的時候,我們就要結合實際的業務場景考慮要不要進行分庫分表了。所以如果單個AOF文件太大,那么是不是可以考慮將其按照固定大小進行拆分,這樣可以避免單個AOF文件過大的問題。那么Redis小于7.0版本為什么沒有采用這種方案呢?主要是這種方案并不符合Redis追求簡單高效的設計思想。假設采用了這種數據分塊的方式,那必定需要實現文件大小檢測、文件創建、文件索引維護等等一系列技術細節問題,對于低版本的Redis來說這些都太繁瑣了,還不如一個AOF文件來的爽快。
PS:在最新的Redis 7.0版本中,Redis已經支持多AOF文件分片機制,原始的單個AOF文件會被拆分為一個基礎文件以及多個增量文件。新版本中之所以開始支持多文件存儲,我想也是隨著業務發展內存數據可能會很龐大,Redis設計者發現如果還是使用單文件存儲,大AOF文件操作以及數據恢復都是一個挑戰。
AOF重寫
既然進行文件切割太繁瑣了,那么就單個AOF文件來說怎么才能減小文件大小呢?那就要從AOF文件的記錄內容入手,通過上文我們了解到AOF文件中實際存儲了修改內存數據的操作命令,因此我們在分析完這些操作命令之后發現,當多條命令操作同一個key的時候,實際我們需要的是最新的一條操作命令,除此之外的歷史操作命令我們并不需要關心。比如【set mufeng handsome】、【set mufeng cool】,如果先后執行了這兩個命令,那么在最終恢復數據的時候,只要恢復【set mufeng cool】即可。因此AOF重寫的本質就是合并命令,也就是說將多條對同一key進行操作的命令進行合并,實際就是使用最新的key值操作命令來代替之前所有關于這個key值的命令。
Redis通過fork子進程來完成AOF文件重寫,因此在講AOF重寫過程之前,我們需要先了解下什么是fork子進程的原理,這樣更加有利于我們后面了解AOF文件重寫的過程。
什么是fork?
fork函數是linux內核提供給用戶創建進程的API,應用程序通過調用fork函數創建子進程,這個子進程可以和原來父進程干同樣的事情,也可以和原來主進程干不同的事情,這主要取決于對應的參數。這個過程就好比孫悟空拔了一根自己的猴毛變出來一個和自己一模一樣的孫悟空。
因此在fork子進程的過程之中,子進程復制了父進程的代碼段、數據段、堆棧、頁表等,同時子進程擁有獨立的虛擬內存空間(當然是從父進程那里復制過來的)。如下所示,實際上fork()最終調用的是內核copy_process方法復制進程。?
父進程fork子進程的時候,子進程擁有獨立的虛擬內存空間,那么對應的物理內存空間是不是也是獨立的呢?我們都知道在計算機中,內存屬于非常寶貴的系統資源,所以大佬們在設計的時候都盡可能的減少內存空間占用從而提高系統資源利用率。fork子進程過程中用到的Copy-On-Write就是典型的內存資源管理優化機制,如果子進程只是讀取數據不進行任何的數據寫入,那么就和父進程公用內存空間。當子進程需要進行數據寫入的時候,發現沒有內控空間可以寫入,此時會觸發一個系統中斷來分配內存空間給子進程進行數據寫入。
什么時機觸發AOF重寫?
執行bgrewriteaof 命令
當我們在客戶端手動執行bgrewriteaof 命令后,可以觸發AOF文件進行重寫,對應Redis源碼中進行重寫的bgrewriteaofCommand 函數會檢測檢測是否滿足進行重寫的條件,主要檢測以下兩個條件:
【Condition1】:檢測當前是否存在已經在執行的AOF重寫子進程,如果存在的話Redis將不再執行AOF文件重寫。
【Condition2】:檢測當前是否存在已經在創建RDB文件的子進程,如果存在的話Redis將AOF文件重寫任務置為待調度狀態,后續如果滿足了重寫條件,則繼續執行AOF文件重寫任務。
也就是說,Redis檢測到當前既沒有AOF重寫子進程也沒有RDB文件創建子進程,那么就可以進行AOF文件重寫。對應源碼如下:
超出配置閾值
如果Redis實例開啟了AOF配置,同時配置了auto-aof-rewrite-percentage以及auto-aof-rewrite-min-size,如果超出了閾值會觸發AOF重寫。
aof_rewrite_scheduled被設置為待調度狀態
在bgrewriteaofCommand函數中,如果當前正在執行RDB dump操作,那么對應的aof待調度aof_rewrite_scheduled狀態就會被置為1,當前RDB dump完成之后,會繼續執行AOF重寫操作。?
AOF重寫過程是怎樣的?
通過上文的描述,我們知道了Redis觸發AOF重寫的時機,那么當觸發重寫之后的具體業務是怎樣的呢?我們一起看下AOF重寫的大致流程:
(1)Redis主進程首先檢查是不是存在rdb dump進程或者aof重寫進程正在運行,如果不存在Redis主進程fork子進程進行aof文件重寫;
(2)fork出來的子進程和原來的Redis主進程擁有同樣的內存數據,子進程遍歷此時的內存數據同時將內存數據寫入到臨時的AOF文件中;
(3)主進程此時仍然可以接收客戶端請求,同時將新的緩存操作寫入aof_buf以及aof_rewrite_buf中,根據對應的同步策略,將buf中的數據分別寫入舊AOF文件以及臨時AOF文件中;
(4)重寫完成之后,臨時AOF文件將替換原有的老的AOF文件,從而完成整個AOF重寫。
AOF模式優點
1、AOF的持久化策略更加豐富些,可以根據實際業務需要進行配置,因此相對來說在數據可靠性方面要更加有優勢一點。
2、AOF文件內容比較好理解,更加方便理解業務緩存數據。
AOF模式缺點
1、通常情況下,同樣的緩存數據,AOF文件比RDB文件大小要大一些。
2、在文件恢復場景下,AOF要比DRB恢復數據慢一些。
RDB持久化
RDB(Redis Data Base),所謂的Redis內存數據快照就是某一時刻Redis存于內存中的所有緩存數據,這就好比用手機相機拍照,記錄當時的美好畫面。Redis可以實現在固定時間間隔后將內存中的緩存數據持久化保存起來。這樣即便是服務器宕機或者重啟了,只要RDB快照文件還存在,快照文件中對應的緩存數據就不會丟失,Redis重新啟動后會重新加載RDB文件到內存中,快速恢復緩存數據,通過這樣的方式保障了緩存數據的可靠性。
RDB文件生成過程
我們以bgsave為例子來看下Redis生成RDB文件的大致過程是怎樣的。
(1)Redis主進程首先判斷當前是否存在已經在執行的aof重寫子進程以及rdb文件生成子進程,如果存在的話則直接進行返回。為什么要進行這樣的判斷呢?主要還是從服務器性能方面進行考量,如果服務器有多個子線程在進行RDB持久化操作,那么必定會對磁盤造成比較大的IO壓力,如果服務器中還部署了其他服務甚至會影響其他服務的正常運行。
(2)Redis主進程fork子進程進行RDB文件生成操作,在fork的過程中,此時的Redis主進程是阻塞的,不能響應客戶端請求,子進程fork完成之后可以繼續響應客戶端請求。
(3)fork出來的子進程遍歷內存數據進行RDB文件生成操作。
(4)如果此時客戶端的請求需要修改緩存數據,那么如上面fork子進程的原理,通過COW機制,操作系統會開辟新的內存空間給Redis主進程進行新的緩存數據寫入。
(5)子進程快照數據生成完成之后,替換原來老的RDB文件。
RDB觸發時機
Redis主要支持兩種持久化操作來生成RDB文件,分別是save、bsave命令方式手動生成以及在配置文件中配置時間間隔自動進行RDB文件生成。
手動命令觸發
客戶端連接到redis之后我們可以通過save以及bsave命令進行RDB文件的立即創建,兩者的區別如下:
save:通過主線程觸發,會阻塞Redis業務,如果內存數據比較多的話,會導致長時間不能響應外部請求;
bsave:客戶端執行bsave命令進行RDB持久化,Redis主線程會fork子線程出來進行RDB文件持久化操作,這樣避免了主線程的阻塞即便正在持久化操作依然可以響應外部數據緩存請求。
不過這里值得注意的是,雖然fork子進程之后不會阻塞主進程,但是在fork的過程中會阻塞主進程,尤其是在內存數據比較大的時候,阻塞主進程的時間會更長。
配置自動觸發
另外在Redis的配置文件redis.conf中,我們可以配置按照一定的時間間隔來進行RDB持久化操作。如下配置:
save 900 1
save 300 10
save 60 10000?
其他的觸發RDB文件生成的操作這里不再贅述了,像從節點執行全量數據同步的時候,也會觸發主節點生成RDB文件發送給從節點。
RDB有沒有丟數據的風險?
大家不妨思考下通過RDB文件進行緩存數據持久化會有什么問題?存不存在丟失緩存數據的風險?這種方式看上去是個還不錯的持久化解決方案,但是實際上隱藏著一些丟失緩存數據的風險。為什么這么說呢?通過分析RDB文件生成的機制我們可以發現有兩個地方存在緩存數據丟失的可能性。
場景1:
由于Redis保存RDB快照文件的策略是按照配置的時間間隔進行持久化保存,也就是每隔一個時間間隔Redis就會保存一個RDB文件。因此在內存數據有更新但是RDB保存時間尚未到來的這段時間如果存在服務器宕機或者服務器重啟的情況,此時內存的數據就會存在丟失的風險,因為Redis還沒來得及將數據持久化到RDB文件中。
場景1中最大的問題就RDB文件持久化存在時間間隔,而這個時間間隔導致了新增的緩存數據存在丟失的風險。那么是不是將時間間隔降低到最小就可以了,比如一秒鐘,即使在這一秒鐘期間出現異常情況,那緩存數據也只是丟掉這一秒鐘的緩存數據,相對來說數據丟失的情況可控一點。但是問題是如果真的每隔1s就保存一個RDB文件到服務器磁盤中,那不論是對Redis本身還是Redis所在的服務器磁盤IO都是一種負擔。
場景2:
?隨著業務的不斷發展,內存中的數據必定會越來越大,因此在fork子進程來生成RDB文件的過程中,需要復制的數據會同樣越來越多,耗費的時間也會越來越多,進而阻塞主進程的時間也會越來越多。如果出現長時間阻塞主進程的情況,那么Redis實例必定無法響應客戶端的數據操作請求,最終導致內存數據沒有進行及時更新,從而出現丟失緩存數據的風險。
RDB模式優點
1、相比AOF在恢復數據的時候需要一條條回放操作命令,通過RDB文件恢復數據效率更高;
2、適合全量備份內存數據場景。
3、同樣規模的內存數據,RDB文件數據更加緊湊,磁盤空間占用更小。
4、可以根據不同的時間間隔保存RDB文件,在恢復數據的時候可以更加靈活地選擇對應版本數據進行恢復。
RDB模式缺點
1、由于RDB數據保存存在一定的時間間隔,因此存在丟失緩存數據的風險;
2、fork子進程進行RDB文件生成,由于是一次性生成一個內存快照文件,對于服務器磁盤IO以及Redis本身來說都屬于重操作,可能會對服務器的磁盤IO造成壓力。
混合持久化
既然AOF以及RDB持久化都有這樣或者那樣的不足,那么有沒有一種持久化方案可以兼顧二者的優點來揚長避短呢?從4.0版本開始,Redis支持混合持久化的方式來兼顧效率以及數據可靠性。在Redis配置文件redis.conf中配置混合持久化:
如果配置了混合持久化,那么Redis主進程在fork子進程進行持久化操作的時候,原先的將內存數據轉換為操作命令的過程將替換為使用進行AOF重寫時對應的RDB文件內容直接放入到重寫后的臨時文件中,后面再有新的操作命令,都追加到臨時aof文件中,重寫完成后使用臨時aof文件替換舊的文件。
混合持久化模式優點
1、同時擁有RDB以及AOF機制的優點,在數據可靠性以及數據恢復效率上面達到了很好的平衡。?
混合持久化模式缺點
1、由于Redis從4.0版本才開始支持混合持久化,如果當前平臺中的Redis版本低于4.0,那么就無法使用這個持久化機制,因此兼容性不夠友好;
總結
本文主要分析了Redis AOF、RDB以及混合持久化的內存數據持久化的機制原理,同時分析了兩種持久化方式的優點以及缺點。我想只有理解了中間件的特性機制原理,知道了特性的長處以及不足我們才能設計適合我們平臺的緩存數據持久化策略,從而提升平臺的穩定性。
另外在一些優秀中間件的學習和使用過程中,我們不能僅僅停留在會用的層面,更應該深入底層領會其架構和實現機制的設計思路,只有搞明白設計思路,時刻站在設計者的角度來看待遇到的問題,那么在我們的實際工作中,如果遇到類似的問題我們可以借鑒這些優秀中間件的解決思路來進行問題分析。