Redis專題:萬字長文詳解持久化原理

微信搜索公衆號「碼路印記」

本文將從如下幾個方面介紹Redis持久化機制:html

## 寫在前面面試

本文從總體上詳細介紹Redis的兩種持久化方式,包含工做原理、持久化流程及實踐策略,以及背後的一些理論知識。上一篇文章僅介紹了RDB持久化,可是Redis持久化是一個總體,單獨介紹不成體系,故從新整理。

Redis是一個內存數據庫,全部的數據將保存在內存中,這與傳統的MySQL、Oracle、SqlServer等關係型數據庫直接把數據保存到硬盤相比,Redis的讀寫效率很是高。可是保存在內存中也有一個很大的缺陷,一旦斷電或者宕機,內存數據庫中的內容將會所有丟失。爲了彌補這一缺陷,Redis提供了把內存數據持久化到硬盤文件,以及經過備份文件來恢復數據的功能,即Redis持久化機制。redis

Redis支持兩種方式的持久化:RDB快照和AOF。shell

RDB持久化

RDB快照用官方的話來講:RDB持久化方案是按照指定時間間隔對你的數據集生成的時間點快照(point-to-time snapshot)。它以緊縮的二進制文件保存Redis數據庫某一時刻全部數據對象的內存快照,可用於Redis的數據備份、轉移與恢復。到目前爲止,還是官方的默認支持方案。數據庫

RDB工做原理

既然說RDB是Redis中數據集的時間點快照,那咱們先簡單瞭解一下Redis內的數據對象在內存中是如何存儲與組織的。數組

默認狀況下,Redis中有16個數據庫,編號從0-15,每一個Redis數據庫使用一個redisDb對象來表示,redisDb使用hashtable存儲K-V對象。爲方便理解,我以其中一個db爲例繪製Redis內部數據的存儲結構示意圖。
image.png
時間點快照也就是某一時刻Redis內每一個DB中每一個數據對象的狀態,先假設在這一時刻全部的數據對象再也不改變,咱們就能夠按照上圖中的數據結構關係,把這些數據對象依次讀取出來並寫入到文件中,以此實現Redis的持久化。而後,當Redis重啓時按照規則讀取這個文件中的內容,再寫入到Redis內存便可恢復至持久化時的狀態。緩存

固然,這個前提時咱們上面的假設成立,不然面對一個時刻變化的數據集,咱們無從下手。咱們知道Redis中客戶端命令處理是單線程模型,若是把持久化做爲一個命令處理,那數據集確定時處於靜止狀態。另外,操做系統提供的fork()函數建立的子進程可得到與父進程一致的內存數據,至關於獲取了內存數據副本;fork完成後,父進程該幹嗎幹嗎,持久化狀態的工做交給子進程就好了。安全

很顯然,第一種狀況不可取,持久化備份會致使短期內Redis服務不可用,這對於高HA的系統來說是沒法容忍的。因此,第二種方式是RDB持久化的主要實踐方式。因爲fork子進程後,父進程數據一直在變化,子進程並不與父進程同步,RDB持久化必然沒法保證明時性;RDB持久化完成後發生斷電或宕機,會致使部分數據丟失;備份頻率決定了丟失數據量的大小,提升備份頻率,意味着fork過程消耗較多的CPU資源,也會致使較大的磁盤I/O。服務器

持久化流程

在Redis內完成RDB持久化的方法有rdbSave和rdbSaveBackground兩個函數方法(源碼文件rdb.c中),先簡單說下二者差異:微信

  • rdbSave:是同步執行的,方法調用後就會馬上啓動持久化流程。因爲Redis是單線程模型,持久化過程當中會阻塞,Redis沒法對外提供服務;
  • rdbSaveBackground:是後臺(異步)執行的,該方法會fork出子進程,真正的持久化過程是在子進程中執行的(調用rdbSave),主進程會繼續提供服務;

RDB持久化的觸發必然離不開以上兩個方法,觸發的方式分爲手動和自動。手動觸發容易理解,是指咱們經過Redis客戶端人爲的對Redis服務端發起持久化備份指令,而後Redis服務端開始執行持久化流程,這裏的指令有save和bgsave。自動觸發是Redis根據自身運行要求,在知足預設條件時自動觸發的持久化流程,自動觸發的場景有以下幾個(摘自這篇文章):

  • serverCron中save m n配置規則自動觸發;
  • 從節點全量複製時,主節點發送rdb文件給從節點完成複製操做,主節點會出發bgsave;
  • 執行debug reload命令從新加載redis時;
  • 默認狀況下(未開啓AOF)執行shutdown命令時,自動執行bgsave;

結合源碼及參考文章,我整理了RDB持久化流程來幫助你們有個總體的瞭解,而後再從一些細節進行說明。
image.png
從上圖能夠知道:

  • 自動觸發的RDB持久化是經過rdbSaveBackground以子進程方式執行的持久化策略;
  • 手動觸發是以客戶端命令方式觸發的,包含save和bgsave兩個命令,其中save命令是在Redis的命令處理線程以阻塞的方式調用rdbSave方法完成的。

自動觸發流程是一個完整的鏈路,涵蓋了rdbSaveBackground、rdbSave等,接下來我以serverCron爲例分析一下整個流程。

save規則及檢查

serverCron是Redis內的一個週期性函數,每隔100毫秒執行一次,它的其中一項工做就是:根據配置文件中save規則來判斷當前須要進行自動持久化流程,若是知足條件則嘗試開始持久化。瞭解一下這部分的實現。

redisServer中有幾個與RDB持久化有關的字段,我從代碼中摘出來,中英文對照着看下:

struct redisServer {
    /* 省略其餘字段 */ 
    /* RDB persistence */
    long long dirty;                /* Changes to DB from the last save
                                     * 上次持久化後修改key的次數 */
    struct saveparam *saveparams;   /* Save points array for RDB,
                                     * 對應配置文件多個save參數 */
    int saveparamslen;              /* Number of saving points,
                                     * save參數的數量 */
    time_t lastsave;                /* Unix time of last successful save 
                                     * 上次持久化時間*/
    /* 省略其餘字段 */
}

/* 對應redis.conf中的save參數 */
struct saveparam {
    time_t seconds;                    /* 統計時間範圍 */   
    int changes;                    /* 數據修改次數 */
};

saveparams對應redis.conf下的save規則,save參數是Redis觸發自動備份的觸發策略,seconds爲統計時間(單位:秒), changes爲在統計時間內發生寫入的次數。save m n的意思是:m秒內有n條寫入就觸發一次快照,即備份一次。save參數能夠配置多組,知足在不一樣條件的備份要求。若是須要關閉RDB的自動備份策略,可使用save ""。如下爲幾種配置的說明:

# 表示900秒(15分鐘)內至少有1個key的值發生變化,則執行
save 900 1
# 表示300秒(5分鐘)內至少有1個key的值發生變化,則執行
save 300 10
# 表示60秒(1分鐘)內至少有10000個key的值發生變化,則執行
save 60 10000
# 該配置將會關閉RDB方式的持久化
save ""

serverCron對RDB save規則的檢測代碼以下所示:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    /* 省略其餘邏輯 */
    
    /* 若是用戶請求進行AOF文件重寫時,Redis正在執行RDB持久化,Redis會安排在RDB持久化完成後執行AOF文件重寫,
     * 若是aof_rewrite_scheduled爲true,說明須要執行用戶的請求 */
    /* Check if a background saving or AOF rewrite in progress terminated. */
    if (hasActiveChildProcess() || ldbPendingChildren())
    {
        run_with_period(1000) receiveChildInfo();
        checkChildrenDone();
    } else {
        /* 後臺無 saving/rewrite 子進程纔會進行,逐個檢查每一個save規則*/
        for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;
            
            /* 檢查規則有幾個:知足修改次數,知足統計週期,達到重試時間間隔或者上次持久化完成*/
            if (server.dirty >= sp->changes 
                && server.unixtime-server.lastsave > sp->seconds 
                &&(server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK))
            {
                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...", sp->changes, (int)sp->seconds);
                rdbSaveInfo rsi, *rsiptr;
                rsiptr = rdbPopulateSaveInfo(&rsi);
                /* 執行bgsave過程 */
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
        }

        /* 省略:Trigger an AOF rewrite if needed. */
    }
    /* 省略其餘邏輯 */
}

若是沒有後臺的RDB持久化或AOF重寫進程,serverCron會根據以上配置及狀態判斷是否須要執行持久化操做,判斷依據就是看lastsave、dirty是否知足saveparams數組中的其中一個條件。若是有一個條件匹配,則調用rdbSaveBackground方法,執行異步持久化流程。

rdbSaveBackground

rdbSaveBackground是RDB持久化的輔助性方法,主要工做是fork子進程,而後根據調用方(父進程或者子進程)不一樣,有兩種不一樣的執行邏輯。

  • 若是調用方是父進程,則fork出子進程,保存子進程信息後直接返回。
  • 若是調用方是子進程則調用rdbSave執行RDB持久化邏輯,持久化完成後退出子進程。
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;

    if (hasActiveChildProcess()) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);

    // fork子進程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* Child 子進程:修改進程標題 */
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        // 執行rdb持久化
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCOWInfo(CHILD_TYPE_RDB, 1, "RDB");
        }
        // 持久化完成後,退出子進程
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent 父進程:記錄fork子進程的時間等信息*/
        if (childpid == -1) {
            server.lastbgsave_status = C_ERR;
            serverLog(LL_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid);
        // 記錄子進程開始的時間、類型等。
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
    return C_OK; /* unreached */
}

rdbSave是真正執行持久化的方法,它在執行時存在大量的I/O、計算操做,耗時、CPU佔用較大,在Redis的單線程模型中持久化過程會持續佔用線程資源,進而致使Redis沒法提供其餘服務。爲了解決這一問題Redis在rdbSaveBackground中fork出子進程,由子進程完成持久化工做,避免了佔用父進程過多的資源。

須要注意的是,若是父進程內存佔用過大,fork過程會比較耗時,在這個過程當中父進程沒法對外提供服務;另外,須要綜合考慮計算機內存使用量,fork子進程後會佔用雙倍的內存資源,須要確保內存夠用。經過info stats命令查看latest_fork_usec選項,能夠獲取最近一個fork以操做的耗時。

rdbSave

Redis的rdbSave函數是真正進行RDB持久化的函數,流程、細節賊多,總體流程能夠總結爲:建立並打開臨時文件、Redis內存數據寫入臨時文件、臨時文件寫入磁盤、臨時文件重命名爲正式RDB文件、更新持久化狀態信息(dirty、lastsave)。其中「Redis內存數據寫入臨時文件」最爲核心和複雜,寫入過程直接體現了RDB文件的文件格式,本着一圖勝千言的理念,我按照源碼流程繪製了下圖。
image.png
補充說明一下,上圖右下角「遍歷當前數據庫的鍵值對並寫入」這個環節會根據不一樣類型的Redis數據類型及底層數據結構採用不一樣的格式寫入到RDB文件中,再也不展開了。我以爲你們對整個過程有個直觀的理解就好,這對於咱們理解Redis內部的運做機制大有裨益。

AOF持久化

上一節咱們知道RDB是一種時間點(point-to-time)快照,適合數據備份及災難恢復,因爲工做原理的「先天性缺陷」沒法保證明時性持久化,這對於緩存丟失零容忍的系統來講是個硬傷,因而就有了AOF。

AOF工做原理

AOF是Append Only File的縮寫,它是Redis的徹底持久化策略,從1.1版本開始支持;這裏的file存儲的是引發Redis數據修改的命令集合(好比:set/hset/del等),這些集合按照Redis Server的處理順序追加到文件中。當重啓Redis時,Redis就能夠從頭讀取AOF中的指令並重放,進而恢復關閉前的數據狀態。

AOF持久化默認是關閉的,修改redis.conf如下信息並重啓,便可開啓AOF持久化功能。

# no-關閉,yes-開啓,默認no
appendonly yes
appendfilename appendonly.aof

AOF本質是爲了持久化,持久化對象是Redis內每個key的狀態,持久化的目的是爲了在Reids發生故障重啓後可以恢復至重啓前或故障前的狀態。相比於RDB,AOF採起的策略是按照執行順序持久化每一條可以引發Redis中對象狀態變動的命令,命令是有序的、有選擇的。把aof文件轉移至任何一臺Redis Server,從頭至尾按序重放這些命令便可恢復如初。舉個例子:

首先執行指令set number 0,而後隨機調用incr numberget number 各5次,最後再執行一次get number ,咱們獲得的結果確定是5。

由於在這個過程當中,可以引發number狀態變動的只有set/incr類型的指令,而且它們執行的前後順序是已知的,不管執行多少次get都不會影響number的狀態。因此,保留全部set/incr命令並持久化至aof文件便可。按照aof的設計原理,aof文件中的內容應該是這樣的(這裏是假設,實際爲RESP協議):

set number 0
incr number
incr number
incr number
incr number
incr number

最本質的原理用「命令重放」四個字就能夠歸納。可是,考慮實際生產環境的複雜性及操做系統等方面的限制,Redis所要考慮的工做要比這個例子複雜的多:

  • Redis Server啓動後,aof文件一直在追加命令,文件會愈來愈大。文件越大,Redis重啓後恢復耗時越久;文件太大,轉移工做就越難;不加管理,可能撐爆硬盤。很顯然,須要在合適的時機對文件進行精簡。例子中的5條incr指令很明顯的能夠替換爲爲一條set命令,存在很大的壓縮空間。
  • 衆所周知,文件I/O是操做系統性能的短板,爲了提升效率,文件系統設計了一套複雜的緩存機制,Redis操做命令的追加操做只是把數據寫入了緩衝區(aof_buf),從緩衝區到寫入物理文件在性能與安全之間權衡會有不一樣的選擇。
  • 文件壓縮即意味着重寫,重寫時便可依據已有的aof文件作命令整合,也能夠先根據當前Redis內數據的狀態作快照,再把存儲快照過程當中的新增的命令作追加。
  • aof備份後的文件是爲了恢復數據,結合aof文件的格式、完整性等因素,Redis也要設計一套完整的方案作支持。

持久化流程

從流程上來看,AOF的工做原理能夠歸納爲幾個步驟:命令追加(append)、文件寫入與同步(fsync)、文件重寫(rewrite)、重啓加載(load),接下來依次瞭解每一個步驟的細節及背後的設計哲學。
image.png

命令追加

當 AOF 持久化功能處於打開狀態時,Redis 在執行完一個寫命令以後,會以協議格式(也就是RESP,即 Redis 客戶端和服務器交互的通訊協議 )把被執行的寫命令追加到 Redis 服務端維護的 AOF 緩衝區末尾。對AOF文件只有單線程的追加操做,沒有seek等複雜的操做,即便斷電或宕機也不存在文件損壞風險。另外,使用文本協議好處多多:

  • 文本協議有很好的兼容性;
  • 文本協議就是客戶端的請求命令,不須要二次處理,節省了存儲及加載時的處理開銷;
  • 文本協議具備可讀性,方便查看、修改等處理。

AOF緩衝區類型爲Redis自主設計的數據結構sds,Redis會根據命令的類型採用不一樣的方法(catAppendOnlyGenericCommandcatAppendOnlyExpireAtCommand等)對命令內容進行處理,最後寫入緩衝區。

須要注意的是:若是命令追加時正在進行AOF重寫,這些命令還會追加到重寫緩衝區(aof_rewrite_buffer)。

文件寫入與同步

AOF文件的寫入與同步離不開操做系統的支持,開始介紹以前,咱們須要補充一下Linux I/O緩衝區相關知識。硬盤I/O性能較差,文件讀寫速度遠遠比不上CPU的處理速度,若是每次文件寫入都等待數據寫入硬盤,會總體拉低操做系統的性能。爲了解決這個問題,操做系統提供了延遲寫(delayed write)機制來提升硬盤的I/O性能。
image.png

傳統的UNIX實如今內核中設有緩衝區高速緩存或頁面高速緩存,大多數磁盤I/O都經過緩衝進行。 當將數據寫入文件時,內核一般先將該數據複製到其中一個緩衝區中,若是該緩衝區還沒有寫滿,則並不將其排入輸出隊列,而是等待其寫滿或者當內核須要重用該緩衝區以便存放其餘磁盤塊數據時, 再將該緩衝排入到輸出隊列,而後待其到達隊首時,才進行實際的I/O操做。這種輸出方式就被稱爲延遲寫。

延遲寫減小了磁盤讀寫次數,可是卻下降了文件內容的更新速度,使得欲寫到文件中的數據在一段時間內並無寫到磁盤上。當系統發生故障時,這種延遲可能形成文件更新內容的丟失。爲了保證磁盤上實際文件系統與緩衝區高速緩存中內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函數爲強制寫入硬盤提供支持。

Redis每次事件輪訓結束前(beforeSleep)都會調用函數flushAppendOnlyFileflushAppendOnlyFile會把AOF緩衝區(aof_buf)中的數據寫入內核緩衝區,而且根據appendfsync配置來決定採用何種策略把內核緩衝區中的數據寫入磁盤,即調用fsync()。該配置有三個可選項alwaysnoeverysec,具體說明以下:

  • always:每次都調用fsync(),是安全性最高、性能最差的一種策略。
  • no:不會調用fsync()。性能最好,安全性最差。
  • everysec:僅在知足同步條件時調用fsync()。這是官方建議的同步策略,也是默認配置,作到兼顧性能和數據安全性,理論上只有在系統忽然宕機的狀況下丟失1秒的數據。

注意:上面介紹的策略受配置項no-appendfsync-on-rewrite的影響,它的做用是告知Redis:AOF文件重寫期間是否禁止調用fsync(),默認是no。

若是appendfsync設置爲alwayseverysec,後臺正在進行的BGSAVE或者BGREWRITEAOF消耗過多的磁盤I/O,在某些Linux系統配置下,Redis對fsync()的調用可能阻塞很長時間。然而這個問題尚未修復,由於即便是在不一樣的線程中執行fsync(),同步寫入操做也會被阻塞。

爲了緩解此問題,可使用該選項,以防止在進行BGSAVEBGREWRITEAOF時在主進程中調用fsync()。

  • 設置爲yes意味着,若是子進程正在進行BGSAVEBGREWRITEAOF,AOF的持久化能力就與appendfsync設置爲no有着相同的效果。最糟糕的狀況下,這可能會致使30秒的緩存數據丟失。
  • 若是你的系統有上面描述的延遲問題,就把這個選項設置爲yes,不然保持爲no

文件重寫

如前面提到的,Redis長時間運行,命令不斷寫入AOF,文件會愈來愈大,不加控制可能影響宿主機的安全。

爲了解決AOF文件體積問題,Redis引入了AOF文件重寫功能,它會根據Redis內數據對象的最新狀態生成新的AOF文件,新舊文件對應的數據狀態一致,可是新文件會具備較小的體積。重寫既減小了AOF文件對磁盤空間的佔用,又能夠提升Redis重啓時數據恢復的速度。仍是下面這個例子,舊文件中的6條命令等同於新文件中的1條命令,壓縮效果顯而易見。
image.png
咱們說,AOF文件太大時會觸發AOF文件重寫,那究竟是多大呢?有哪些狀況會觸發重寫操做呢?
**
與RDB方式同樣,AOF文件重寫既能夠手動觸發,也會自動觸發。手動觸發直接調用bgrewriteaof命令,若是當時無子進程執行會馬上執行,不然安排在子進程結束後執行。自動觸發由Redis的週期性方法serverCron檢查在知足必定條件時觸發。先了解兩個配置項:

  • auto-aof-rewrite-percentage:表明當前AOF文件大小(aof_current_size)和上一次重寫後AOF文件大小(aof_base_size)相比,增加的比例。
  • auto-aof-rewrite-min-size:表示運行BGREWRITEAOF時AOF文件佔用空間最小值,默認爲64MB;

Redis啓動時把aof_base_size初始化爲當時aof文件的大小,Redis運行過程當中,當AOF文件重寫操做完成時,會對其進行更新;aof_current_sizeserverCron執行時AOF文件的實時大小。當知足如下兩個條件時,AOF文件重寫就會觸發:

增加比例:(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage
文件大小:aof_current_size > auto-aof-rewrite-min-size

手動觸發與自動觸發的代碼以下,一樣在週期性方法serverCron中:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    /* 省略其餘邏輯 */
    
    /* 若是用戶請求進行AOF文件重寫時,Redis正在執行RDB持久化,Redis會安排在RDB持久化完成後執行AOF文件重寫,
     * 若是aof_rewrite_scheduled爲true,說明須要執行用戶的請求 */
    if (!hasActiveChildProcess() &&
        server.aof_rewrite_scheduled)
    {
        rewriteAppendOnlyFileBackground();
    }

    /* Check if a background saving or AOF rewrite in progress terminated. */
    if (hasActiveChildProcess() || ldbPendingChildren())
    {
        run_with_period(1000) receiveChildInfo();
        checkChildrenDone();
    } else {
        /* 省略rdb持久化條件檢查 */

        /* AOF重寫條件檢查:aof開啓、無子進程運行、增加百分比已設置、當前文件大小超過閾值 */
        if (server.aof_state == AOF_ON &&
            !hasActiveChildProcess() &&
            server.aof_rewrite_perc &&
            server.aof_current_size > server.aof_rewrite_min_size)
        {
            long long base = server.aof_rewrite_base_size ?
                server.aof_rewrite_base_size : 1;
            /* 計算增加百分比 */
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) {
                serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                rewriteAppendOnlyFileBackground();
            }
        }
    }
    /**/
}

AOF文件重寫的流程是什麼?據說Redis支持混合持久化,對AOF文件重寫有什麼影響?

從4.0版本開始,Redis在AOF模式中引入了混合持久化方案,即:純AOF方式、RDB+AOF方式,這一策略由配置參數aof-use-rdb-preamble(使用RDB做爲AOF文件的前半段)控制,默認關閉(no),設置爲yes可開啓。因此,在AOF重寫過程當中文件的寫入會有兩種不一樣的方式。當aof-use-rdb-preamble的值是:

  • no:按照AOF格式寫入命令,與4.0前版本無差異;
  • yes:先按照RDB格式寫入數據狀態,而後把重寫期間AOF緩衝區的內容以AOF格式寫入,文件前半部分爲RDB格式,後半部分爲AOF格式。

結合源碼(6.0版本,源碼太多這裏不貼出,可參考aof.c)及參考資料,繪製AOF重寫(BGREWRITEAOF)流程圖:
image.png
結合上圖,總結一下AOF文件重寫的流程:

  • rewriteAppendOnlyFileBackground開始執行,檢查是否有正在進行的AOF重寫或RDB持久化子進程:若是有,則退出該流程;若是沒有,則繼續建立接下來父子進程間數據傳輸的通訊管道。執行fork()操做,成功後父子進程分別執行不一樣的流程。
  • 父進程:

    • 記錄子進程信息(pid)、時間戳等;
    • 繼續響應其餘客戶端請求;
    • 收集AOF重寫期間的命令,追加至aof_rewrite_buffer;
    • 等待並向子進程同步aof_rewrite_buffer的內容;
  • 子進程:

    • 修改當前進程名稱,建立重寫所需的臨時文件,調用rewriteAppendOnlyFile函數;
    • 根據aof-use-rdb-preamble配置,以RDB或AOF方式寫入前半部分,並同步至硬盤;
    • 從父進程接收增量AOF命令,以AOF方式寫入後半部分,並同步至硬盤;
    • 重命名AOF文件,子進程退出。

數據加載

Redis啓動後經過loadDataFromDisk函數執行數據加載工做。這裏須要注意,雖然持久化方式能夠選擇AOF、RDB或者二者兼用,可是數據加載時必須作出選擇,兩種方式各自加載一遍就亂套了。

理論上,AOF持久化比RDB具備更好的實時性,當開啓了AOF持久化方式,Redis在數據加載時優先考慮AOF方式。並且,Redis 4.0版本後AOF支持了混合持久化,加載AOF文件須要考慮版本兼容性。Redis數據加載流程以下圖所示:
image.png
在AOF方式下,開啓混合持久化機制生成的文件是「RDB頭+AOF尾」,未開啓時生成的文件所有爲AOF格式。考慮兩種文件格式的兼容性,若是Redis發現AOF文件爲RDB頭,會使用RDB數據加載的方法讀取並恢復前半部分;而後再使用AOF方式讀取並恢復後半部分。因爲AOF格式存儲的數據爲RESP協議命令,Redis採用僞客戶端執行命令的方式來恢復數據。

若是在AOF命令追加過程當中發生宕機,因爲延遲寫的技術特色,AOF的RESP命令可能不完整(被截斷)。遇到這種狀況時,Redis會按照配置項aof-load-truncated執行不一樣的處理策略。這個配置是告訴Redis啓動時讀取aof文件,若是發現文件被截斷(不完整)時該如何處理:

  • yes:則儘量多的加載數據,並以日誌的方式通知用戶;
  • no:則以系統錯誤的方式崩潰,並禁止啓動,須要用戶修復文件後再重啓。

總結

Redis提供了兩種持久化的選擇:RDB支持以特定的實踐間隔爲數據集生成時間點快照;AOF把Redis Server收到的每條寫指令持久化到日誌中,待Redis重啓時經過重放命令恢復數據。日誌格式爲RESP協議,對日誌文件只作append操做,無損壞風險。而且當AOF文件過大時能夠自動重寫壓縮文件。

固然,若是你不須要對數據進行持久化,也能夠禁用Redis的持久化功能,可是大多數狀況並不是如此。實際上,咱們時有可能同時使用RDB和AOF兩種方式的,最重要的就是咱們要理解二者的區別,以便合理使用。

RDB vs AOF

RDB優勢

  • RDB是一個緊湊壓縮的二進制文件,表明Redis在某一個時間點上的數據快照,很是適合用於備份、全量複製等場景。
  • RDB對災難恢復、數據遷移很是友好,RDB文件能夠轉移至任何須要的地方並從新加載。
  • RDB是Redis數據的內存快照,數據恢復速度較快,相比於AOF的命令重放有着更高的性能。

RDB缺點

  • RDB方式沒法作到實時或秒級持久化。由於持久化過程是經過fork子進程後由子進程完成的,子進程的內存只是在fork操做那一時刻父進程的數據快照,而fork操做後父進程持續對外服務,內部數據時刻變動,子進程的數據再也不更新,二者始終存在差別,因此沒法作到實時性。
  • RDB持久化過程當中的fork操做,會致使內存佔用加倍,並且父進程數據越多,fork過程越長。
  • Redis請求高併發可能會頻繁命中save規則,致使fork操做及持久化備份的頻率不可控;
  • RDB文件有文件格式要求,不一樣版本的Redis會對文件格式進行調整,存在老版本沒法兼容新版本的問題。

AOF優勢

  • AOF持久化有更好的實時性,咱們能夠選擇三種不一樣的方式(appendfsync):no、every second、always,every second做爲默認的策略具備最好的性能,極端狀況下可能會丟失一秒的數據。
  • AOF文件只有append操做,無複雜的seek等文件操做,沒有損壞風險。即便最後寫入數據被截斷,也很容易使用redis-check-aof工具修復;
  • 當AOF文件變大時,Redis可在後臺自動重寫。重寫過程當中舊文件會持續寫入,重寫完成後新文件將變得更小,而且重寫過程當中的增量命令也會append到新文件。
  • AOF文件以已於理解與解析的方式包含了對Redis中數據的全部操做命令。即便不當心錯誤的清除了全部數據,只要沒有對AOF文件重寫,咱們就能夠經過移除最後一條命令找回全部數據。
  • AOF已經支持混合持久化,文件大小能夠有效控制,並提升了數據加載時的效率。

AOF缺點

  • 對於相同的數據集合,AOF文件一般會比RDB文件大;
  • 在特定的fsync策略下,AOF會比RDB略慢。通常來說,fsync_every_second的性能仍然很高,fsync_no的性能與RDB至關。可是在巨大的寫壓力下,RDB更能提供最大的低延時保障。
  • 在AOF上,Redis曾經遇到一些幾乎不可能在RDB上遇到的罕見bug。一些特殊的指令(如BRPOPLPUSH)致使從新加載的數據與持久化以前不一致,Redis官方曾經在相同的條件下進行測試,可是沒法復現問題。

使用建議

對RDB和AOF兩種持久化方式的工做原理、執行流程及優缺點了解後,咱們來思考下,實際場景中應該怎麼權衡利弊,合理的使用兩種持久化方式。若是僅僅是使用Redis做爲緩存工具,全部數據能夠根據持久化數據庫進行重建,則可關閉持久化功能,作好預熱、緩存穿透、擊穿、雪崩之類的防禦工做便可。

通常狀況下,Redis會承擔更多的工做,如分佈式鎖、排行榜、註冊中心等,持久化功能在災難恢復、數據遷移方面將發揮較大的做用。建議遵循幾個原則:

  • 不要把Redis做爲數據庫,全部數據儘量可由應用服務自動重建。
  • 使用4.0以上版本Redis,使用AOF+RDB混合持久化功能。
  • 合理規劃Redis最大佔用內存,防止AOF重寫或save過程當中資源不足。
  • 避免單機部署多實例。
  • 生產環境多爲集羣化部署,可在slave開啓持久化能力,讓master更好的對外提供寫服務。
  • 備份文件應自動上傳至異地機房或雲存儲,作好災難備份。

關於fork()

經過上面的分析,咱們都知道RDB的快照、AOF的重寫都須要fork,這是一個重量級操做,會對Redis形成阻塞。所以爲了避免影響Redis主進程響應,咱們須要儘量下降阻塞。

  • 下降fork的頻率,好比能夠手動來觸發RDB生成快照、與AOF重寫;
  • 控制Redis最大使用內存,防止fork耗時過長;
  • 使用更高性能的硬件;
  • 合理配置Linux的內存分配策略,避免由於物理內存不足致使fork失敗。

參考文獻

相關文章
相關標籤/搜索