[Redis源碼閱讀]redis持久化

做爲web開發的一員,相信你們的面試經歷裏少不了會遇到這個問題:redis是怎麼作持久化的?php

不急着給出答案,先停下來思考一下,而後再看看下面的介紹。但願看了這邊文章後,你可以回答這個問題。html

爲何須要持久化?

因爲Redis是一種內存型數據庫,即服務器在運行時,系統爲其分配了一部份內存存儲數據,一旦服務器掛了,或者忽然宕機了,那麼數據庫裏面的數據將會丟失,爲了使服務器即便忽然關機也能保存數據,必須經過持久化的方式將數據從內存保存到磁盤中。web

對於進行持久化的程序來講,數據從程序寫到計算機的磁盤的流程以下:面試

一、客戶端發送一個寫指令給數據庫(此時數據在客戶端的內存)redis

二、數據庫接收到寫的指令以及數據(數據此時在服務端的內存)數據庫

三、數據庫發起一個系統調用,把數據寫到磁盤(此時數據在內核的內存)緩存

四、操做系統把數據傳輸到磁盤控制器(數據此時在磁盤緩存中)安全

五、磁盤控制器執行真正寫入數據到物理媒介的操做(如磁盤)服務器

若是隻是考慮數據庫層面,數據在第三階段以後就安全了,在這個時候,系統調用已經發起了,即便數據庫進程奔潰了,系統調用會繼續進行,也能順利將數據寫入到磁盤中。
在這一步以後,在第4步內核會將數據從內核緩存保存到磁盤緩存中,但爲了系統的效率問題,默認狀況下不會太頻繁地執行這個動做,大概會在30s執行一次,這就意味着若是這一步失敗了或者就在進行這一步的時候服務器忽然關機了,那麼就可能會有30s的數據丟失了,這種比較普通的災難性問題也是須要考慮的。app

POSIX API也提供了一個系統調用讓內核強制將緩存數據寫入到磁盤中,比較常見的就是fsync系統調用。

int fsync(int fd);

fsync函數只對由文件描述符fd指定的一個文件起做用,而且等待寫磁盤操做結束後才返回。每次調用fsync時,會初始化一個寫操做,而後把緩衝區的數據寫入到磁盤中。fsync()函數在完成寫操做的時候會阻塞進程,若是其餘線程也在寫同一個文件,它也會阻塞其餘線程,直到完成寫操做。

持久化

持久化是將程序數據在持久狀態和瞬時狀態間轉換的機制。對於程序來講,程序運行中數據是在內存的,若是沒有及時同步寫入到磁盤,那麼一旦斷電或者程序忽然奔潰,數據就會丟失了,只有把數據及時同步到磁盤,數據才能永久保存,不會由於宕機影像數據的有效性。而持久化就是將數據從程序同步到磁盤的一個動做過程。

持久化

Redis的持久化

redis有RDB和AOF兩種持久化方式。RDB是快照文件的方式,redis經過執行SAVE/BGSAVE命令,執行數據的備份,將redis當前的數據保存到*.rdb文件中,文件保存了全部的數據集合。AOF是服務器經過讀取配置,在指定的時間裏,追加redis寫操做的命令到*.aof文件中,是一種增量的持久化方式。

RDB

RDB文件經過SAVE或BGSAVE命令實現。
SAVE命令會阻塞Redis服務進程,直到RDB文件建立完成爲止。
BGSAVE命令經過fork子進程,有子進程來進行建立RDB文件,父進程和子進程共享數據段,父進程繼續提供讀寫服務,子進程實現備份功能。BGSAVE階段只有在須要修改共享數據段的時候才進行拷貝,也就是COW(Copy On Write)。SAVE建立RDB文件能夠經過設置多個保存條件,只要其中一個條件知足,就能夠在後臺執行SAVE操做。

SAVE和BGSAVE命令的實現代碼以下:

void saveCommand(client *c) {
    // BGSAVE執行時不能執行SAVE
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    rdbSaveInfo rsi, *rsiptr;
    rsiptr = rdbPopulateSaveInfo(&rsi);
    // 調用rdbSave函數執行備份(阻塞當前客戶端)
    if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
        addReply(c,shared.ok);
    } else {
        addReply(c,shared.err);
    }
}

/*
* BGSAVE 命令實現 [可選參數"schedule"]
*/
void bgsaveCommand(client *c) {
    int schedule = 0;

    /* 當AOF正在執行時,SCHEDULE參數修改BGSAVE的效果
    * BGSAVE會在以後執行,而不是報錯
    * 能夠理解爲:BGSAVE被提上日程
    */
    if (c->argc > 1) {
        // 參數只能是"schedule"
        if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
            schedule = 1;
        } else {
            addReply(c,shared.syntaxerr);
            return;
        }
    }

    // BGSAVE正在執行,不操做
    if (server.rdb_child_pid != -1) {
        addReplyError(c,"Background save already in progress");
    } else if (server.aof_child_pid != -1) {
        // aof正在執行,若是schedule==1,BGSAVE被提上日程
        if (schedule) {
            server.rdb_bgsave_scheduled = 1;
            addReplyStatus(c,"Background saving scheduled");
        } else {
            addReplyError(c,
            "An AOF log rewriting in progress: can't BGSAVE right now. "
            "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
            "possible.");
        }
    } else if (rdbSaveBackground(server.rdb_filename,NULL) == C_OK) {// 不然調用rdbSaveBackground執行備份操做
        addReplyStatus(c,"Background saving started");
    } else {
        addReply(c,shared.err);
    }
}

有了RDB文件以後,若是服務器關機了,或者須要新增一個服務器,從新啓動數據庫服務器以後,就能夠經過載入RDB文件恢復以前備份的數據。
可是bgsave會耗費較長時間,不夠實時,會致使在停機的時候丟失大量數據。

AOF(Append Only File)

RDB文件保存的是數據庫的鍵值對數據,AOF保存的是數據庫執行的寫命令。

AOF的實現流程有三步:

append->write->fsync

append追加命令到AOF緩衝區,write將緩衝區的內容寫入到程序緩衝區,fsync將程序緩衝區的內容寫入到文件。
當AOF持久化功能處於開啓狀態時,服務器每執行完一個命令,就會將命令以協議格式追加寫入到redisServer結構體的aof_buf緩衝區,具體的協議這裏不展開闡述。

AOF的持久化發生時期有個配置選項:appendfsync。該選項有三個值:
always:全部內容寫入並同步到aof文件
everysec:將aof_buf緩衝區的內容寫入到AOF文件,若是距離上次同步AOF文件的
no:將aof_buf緩衝區中的全部內容寫入到AOF文件,但並不對AOF文件進行同步,由操做系統決定什麼時候進行同步,通常是默認狀況下的30s。

AOF持久化模式每一個寫命令都會追加到AOF文件,隨着服務器不斷運行,AOF文件會愈來愈大,爲了不AOF產生的文件太大,服務器會對AOF文件進行重寫,將操做相同key的相同命令合併,從而減小文件的大小。

舉個例子,要保存一個員工的名字、性別等信息:

> hset employee_12345 name "hoohack"
> hset employee_12345 good_at "php"
> hset employee_12345 gender "male"

只是錄入這個哈希鍵的狀態,AOF文件就須要保存三條命令,若是還有其餘,好比刪除,或者更新值的操做,那命令將會更多,文件會更大,有了重寫後,就能夠適當地減小文件的大小。

AOF重寫的實現原理是先服務器中的數據庫,而後遍歷數據庫,找出每一個數據庫中的全部鍵對象,獲取鍵值對的鍵和值,根據鍵的類型對鍵值對進行重寫。好比上面的例子,能夠合併爲下面的一條命令:

> hset employee_12345 name "hoohack" good_at "php" gender "male"。

AOF的重寫會執行大量的寫入操做,Redis是單線程的,因此若是有服務器直接調用重寫,服務器就不能處理其餘命令了,所以Redis服務器新起了單獨一個進程來執行AOF重寫。

Redis執行重寫的流程:
redis rewrite

在子進程執行AOF重寫時,服務端接收到客戶端的命令以後,先執行客戶端發來的命令,而後將執行後的寫命令追加到AOF緩衝區中,同時將執行後的寫命令追加到AOF重寫緩衝區中。
等到子進程完成了重寫工做後,會發一個完成的信號給服務器,服務器就將AOF重寫緩衝區中的全部內容追加到AOF文件中,而後原子性地覆蓋現有的AOF文件。

RDB和AOF的優缺點

RDB持久化方式能夠只經過服務器讀取數據就能加載備份中的文件到程序中,而AOF方式必須建立一個僞客戶端才能執行。

RDB的文件較小,保存了某個時間點以前的數據,適合作災備和主從同步。

RDB備份耗時較長,若是數據量大,在遇到宕機的狀況下,可能會丟失部分數據。另外,RDB是經過配置使達到某種條件的時候才執行,若是在這段時間內宕機,那麼這部分數據也會丟失。

AOF方式,在相同數據集的狀況下,文件大小會比RDB方式的大。

AOF的持久化方式也是經過配置的不一樣,默認配置的是每秒同步,最快的模式是同步每個命令,最壞的方式是等待系統執行fsync將緩衝同步到磁盤文件中,大部分操做系統是30s。一般狀況下會配置爲每秒同步一次,因此最多會有1s的數據丟失。

怎樣的同步方式更好?

RDB和AOF方式結合。起一個定時任務,每小時備份一份服務器當前狀態的數據,以日期和小時命名,另外起一個定時任務,定時刪除無效的備份文件(好比48小時以前)。AOF配置爲1s一次。這樣一來,最多會丟失1s的數據,同時若是redis發生雪崩,也能迅速恢復爲前一天的狀態,不至於中止服務。

總結

Redis的持久化方案也不是一成不變的,紙上的理論還須要結合實踐成果來證實其可行性。

原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。

更多精彩內容,請關注我的公衆號。

參考文章:
http://oldblog.antirez.com/post/redis-persistence-demystified.html
http://blog.httrack.com/blog/2013/11/15/everything-you-always-wanted-to-know-about-fsync/

相關文章
相關標籤/搜索