<code>Redis</code>作為記憶體型的資料庫,雖然很快,依然有着很大的隐患,一旦伺服器當機重新開機,記憶體中資料還會存在嗎?
很容易想到的一個方案是從背景資料恢複這些資料,如果資料量很小,這倒是一個可行的方案。但是如果資料量過大,頻繁的從背景資料庫通路資料,壓力很大;另外一方面恢複資料的時間極慢。
對于<code>Redis</code>來說,實作資料的持久化和快速恢複是至關重要。
今天這篇文章就來介紹一下<code>Redis</code>持久化的兩種機制<code>AOF</code>日志、<code>RDB</code>快照。
<code>AOF</code>(<code>Append Only File</code>)日志稱之為寫後日志,即是指令先執行完成,把資料寫入記憶體,然後才會記錄日志。
<code>AOF</code>日志(文本形式)會将收到每一條的指令且執行成功的指令以一定的格式寫入到文本中(追加的方式)。
寫後日志有什麼好處呢? 如下:
對于寫前日志無論指令是否執行成功都會被記錄,但是<code>Redis</code>的寫後日志則隻有指令執行成功才會被寫入日志,避免了日志中存在錯誤指令;
同時由于是指令執行成功之後才會寫入日志,是以不會阻塞目前指令的執行。
但是<code>AOF</code>日志也有潛在的風險,分析如下:
由于是寫後日志,如果在指令執行成功之後,在日志未寫入磁盤之前伺服器突然當機,那重新開機恢複資料的時候,這部分的資料肯定在日志檔案中不存在了,那麼将會丢失。(無法通過背景資料庫恢複的情況下)
雖然不會阻塞目前指令的執行,由于記錄日志也是在主線程中(<code>Redis</code>是單線程),如果日志寫入磁盤的時候突然阻塞了,肯定會影響下一個指令的執行。
為了解決上面的風險,<code>AOF</code>日志提供了三種回寫政策。
<code>AOF</code>機制提供了三種回寫政策,這些都在<code>appendfsync</code>配置,如下:
<code>Always</code>(同步寫回):指令執行完成,立馬同步的将日志寫入磁盤
<code>Everysec</code>(每秒寫回):指令執行完成後,先将日志寫入 AOF 檔案的記憶體緩沖區,每隔一秒把緩沖區中内容寫入磁盤。
<code>No</code>(作業系統控制的寫回):每個寫指令執行完,隻是先把日志寫到<code>AOF</code>檔案的記憶體緩沖區,由作業系統決定何時将緩沖區内容寫回磁盤。
其實這三中寫回政策都無法解決主線程的阻塞和資料丢失的問題,分析如下:
<code>同步寫回</code>:基本不丢失資料,但是每步操作都會有一個慢速的落盤操作,不可避免的影響主線程性能。
<code>每秒寫回</code>:采用一秒寫一次到 AOF 日志檔案中,但是一旦當機還是會丢失一秒的資料。
<code>作業系統控制的寫回</code>:在寫完緩沖區之後則會寫入磁盤,但是資料始終在緩沖區的時間内一旦當機,資料還是會丢失。
以上三種政策優缺點總結如下表:
政策
優點
缺點
Always
可靠性高,資料基本不丢失
每個寫指令都要落盤,性能影響較大
Everysec
性能适中
當機時丢失一秒資料
No
性能好
當機時丢失資料較多
随着資料量的增大,AOF日志檔案難免會很大,這樣将會導緻寫入和恢複資料都将變得非常慢。此時AOF提供了一種重寫機制解決這一問題。
重寫機制了解起來很簡單,即是<code>Redis</code>會建立一個新的<code>AOF</code>日志檔案,将每個鍵值對最終的值用一條指令寫入日志檔案中。
比如讀取了鍵值對<code>key1:value1</code>,重寫機制會在新的AOF日志檔案中記錄如下一條指令:
1
set key1 value1
其實即是記錄多次修改的最終的值記錄在新的AOF日志檔案中,這樣當恢複資料時可直接執行該指令。
為什麼重寫機制能夠縮小檔案呢? 當一個鍵值被多次修改後,<code>AOF</code>日志檔案中将會記錄多次修改鍵值的指令,重寫機制是根據這個鍵值最新狀态為它生成寫入指令,這樣舊檔案中的多條指令在重寫後的新日志中變成了一條指令。
作者畫了一張重寫流程圖,僅供參考,如下:

AOF重寫雖然能夠縮減日志檔案的大小,達到減少日志記錄和資料恢複的時間,但是在資料量非常的大情況下把整個資料庫重寫後的日志寫入磁盤是一個非常耗時的過程,難道不會阻塞主線程嗎?
答案是:不會阻塞主線程; 因為AOF重寫過程是由背景子程序<code>bgrewriteaof</code>來完成的,這也是為了避免阻塞主線程,導緻資料庫性能下降。
其實重寫的過程分為兩個階段:一個拷貝,兩處日志。
一個拷貝:指每次執行重寫時,主線程都<code>fork</code>一個子線程<code>bgrewriteaof</code>,主線程會把記憶體資料拷貝一份到子線程,此時子線程中包含了資料庫的最新資料。然後子線程就能在不影響主線程的情況下進行AOF重寫了。
兩處日志是什麼?如下:
<code>第一處日志</code>:子線程重寫并未阻塞主線程,此時主線程仍然會處理請求,此時的AOF日志仍然正在記錄着,這樣即使當機了,資料也是齊全的。第一處日志即是指主線程正在使用的日志。
<code>第二處日志</code>:指新的AOF重寫日志;重寫過程中的操作也會被寫到重寫日志緩沖區,這樣重寫日志也不會丢失最新的操作。等到拷貝資料的所有操作記錄重寫完成後,重寫日志記錄的這些最新操作也會寫入新的 AOF 檔案,以保證資料庫最新狀态的記錄。此時,我們就可以用新的 AOF 檔案替代舊檔案了。
總結:<code>Redis</code>在進行<code>AOF</code>重寫時,會<code>fork</code>一個子線程(不會阻塞主線程)并進行記憶體拷貝用于重寫,然後使用兩個日志保證重寫過程中,新寫入的資料不會丢失。
雖說進行了日志重寫後,AOF日志檔案會縮減很多,但是在資料恢複過程中仍然是一條指令一條指令(由于單線程,隻能順序執行)的執行恢複資料,這個恢複的過程非常緩慢。
AOF這種通過逐一記錄操作指令的日志方式,提供了三種寫回政策保證資料的可靠性,分别是<code>Always</code>、<code>Everysec</code>和<code>No</code>,這三種政策在可靠性上是從高到低,而在性能上則是從低到高。
為了避免日志檔案過大,Redis提供了重寫的機制,每次重寫都fork一個子線程,拷貝記憶體資料進行重寫,将多條指令縮減成一條生成鍵值對的指令,最終重寫的日志作為新的日志。
<code>RDB</code>(Redis DataBase)是另外一種持久化方式:記憶體快照。
<code>RDB</code>記錄的是某一個時刻的記憶體資料,并不是操作指令。
這種方式類似于拍照,隻保留某一時刻的形象。記憶體快照是将某一時刻的狀态以檔案的形式寫入磁盤。這樣即使當機了,資料也不會丢失,這個快照檔案就稱為<code>RDB</code>檔案。
由于記錄的是某個時刻的記憶體資料,資料恢複非常快的,不需要像AOF日志逐一執行記錄的指令。
為了保證資料的可靠性,Redis執行的全量快照,也就是把記憶體中的所有資料都寫到磁盤中。
随着資料量的增大,一次性把全部資料都寫到磁盤中勢必會造成線程阻塞,這就關系到Redis的性能了。
針對線程阻塞的問題Redis提供了兩個指令,如下:
<code>save</code>:在主線程中執行,會導緻主線程阻塞。
<code>bgsave</code>:<code>fork</code>一個子程序,專門用于寫入<code>RDB</code>檔案,避免了主線程的阻塞,這是Redis的預設配置。
這樣就可以使用<code>bgsave</code>指令執行全量快照,既可以保證資料的可靠性也避免了主線程的阻塞。
子線程執行全量快照的同時,主線程仍然在接受着請求,讀資料肯定沒有問題,但是如果個修改了資料,如何能夠保證快照的完整性呢?
舉個栗子:我在<code>T</code>時刻進行全量快照,假設資料量有<code>8G</code>,寫入磁盤的過程至少需要<code>20S</code>,在這<code>20S</code>的時間内,一旦記憶體中的資料發生了修改,則快照的完整性就破壞了。
但是如果在快照時不能修改資料,則對Redis的性能有巨大的影響,對于這個問題,Redis是如何解決的呢?
<code>Redis</code>借助作業系統提供的<code>寫時複制技術</code>(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。
其實很簡單,<code>bgsave</code>指令會<code>fork</code>一個子線程,這個子線程共享所有記憶體的資料,子線程會讀取主線程記憶體中的資料,将他們寫入<code>RDB</code>檔案。
如上圖,對于<code>鍵值對A</code>的讀取并不會影響子線程,但是如果主線程一旦修改記憶體中一塊資料(例如<code>鍵值對D</code>),這塊資料将會被複制一個副本,然後<code>bgsave</code>子線程會将其寫入<code>RDB</code>檔案。
快照隻是記錄某一時刻的資料,一旦時間隔離很久,則伺服器一旦當機,則會丢失那段時間的資料。
比如在<code>T1</code>時間做了一次快照,在<code>T1+t</code>時又做了一次快照,如果在<code>t</code>這個時間段内伺服器突然當機了,則快照中隻儲存了<code>T1</code>時刻的快照,在<code>t</code>時間段内的資料修改未被記錄(丢失)。如下圖:
從上圖明顯可以看出,<code>RDB</code>并不是一個完美的日志記錄方案,隻有讓<code>t</code>時間逐漸縮小,才能保證丢失的資料縮小。
那麼問題來了,時間能夠縮短<code>1秒</code>嗎? 即是每秒執行一次快照。
全量快照是記錄某一個時刻的全部記憶體資料,每秒執行一次的對Redis性能影響巨大,于是增量快照就出來了。
增量快照是指做了一次全量快照之後,後續的快照隻對修改的資料進行快照記錄,這樣可以避免每次都全量快照的開銷。
增量快照的前提是Redis能夠記住修改的資料,這個功能其實開銷也是巨大的,需要儲存完整的鍵值對,這對記憶體的消耗是巨大的。
為了解決這個問題,Redis使用了<code>AOF</code>和<code>RDB</code>混合使用的方式。
這個概念是在<code>Redis4.0</code>提出的,簡單的說就是記憶體快照以一定的頻率執行,比如1小時一次,在兩次快照之間,使用AOF日志記錄這期間的所有指令操作。
混合使用的方式使得記憶體快照不必頻繁的執行,并且AOF記錄的也不是全部的操作指令,而是兩次快照之間的操作指令,不會出現AOF日志檔案過大的情況了,避免了AOF重寫的開銷了。
這個方案既能夠用到的RDB的快速恢複的好處,又能享受都隻記錄操作指令的簡單優勢,強烈建議使用。
<code>RDB</code>記憶體快照記錄的是某一個時刻的記憶體資料,是以能夠快速恢複;<code>AOF</code>和<code>RDB</code>混合使用能夠使得當機後資料快速恢複,又能夠避免<code>AOF</code>日志檔案過大。
本文介紹了兩種資料恢複和持久化的方案,分别是<code>AOF</code>和<code>RDB</code>。
<code>AOF</code>介紹了什麼?如下:
<code>AOF</code>是寫後日志,通過記錄操作指令持久化資料。
由于<code>AOF</code>是在指令執行之後記錄日志,如果在寫入磁盤之前伺服器當機,則會丢失資料;如果寫入磁盤的時候突然阻塞,則會阻塞主線程;為了解決以上問題,AOF機制提供了三種寫回的政策,每種政策都有不同的優缺點。
<code>AOF</code>日志檔案過大怎麼辦?<code>AOF</code>通過<code>fork</code>一個子線程重寫一個新的日志檔案(共享主線程的記憶體,記錄最新資料的寫入指令),同時子線程重寫,避免阻塞主線程。
<code>RDB</code>介紹了什麼?如下:
<code>RDB</code>是記憶體快照,記錄某一個時刻的記憶體資料,而不是操作指令。
<code>Redis</code>提供了兩個指令,分别是<code>save</code>、<code>bgsave</code>來執行全量快照,這兩個指令的差別則是<code>save</code>是在主線程執行,勢必會阻塞主線程,<code>bgsave</code>是在<code>fork</code>一個子線程,共享記憶體。
RDB通過作業系統的寫時複制技術,能夠保證在執行快照的同時主線程能夠修改快照。
由于兩次快照之間是存在間隔的,一旦伺服器當機,則會丢失兩次間隔時刻的資料,<code>Redis4.0</code>開始使用<code>AOF</code>日志記錄兩次快照之間執行的指令(<code>AOF</code>和<code>RDB</code>混合使用)。