[Redis原始碼閱讀]redis持久化

hoohack發表於2018-04-04

作為web開發的一員,相信大家的面試經歷裡少不了會遇到這個問題:redis是怎麼做持久化的?

不急著給出答案,先停下來思考一下,然後再看看下面的介紹。希望看了這邊文章後,你能夠回答這個問題。

為什麼需要持久化?

由於Redis是一種記憶體型資料庫,即伺服器在執行時,系統為其分配了一部分記憶體儲存資料,一旦伺服器掛了,或者突然當機了,那麼資料庫裡面的資料將會丟失,為了使伺服器即使突然關機也能儲存資料,必須通過持久化的方式將資料從記憶體儲存到磁碟中。

對於進行持久化的程式來說,資料從程式寫到計算機的磁碟的流程如下:

1、客戶端傳送一個寫指令給資料庫(此時資料在客戶端的記憶體)

2、資料庫接收到寫的指令以及資料(資料此時在服務端的記憶體)

3、資料庫發起一個系統呼叫,把資料寫到磁碟(此時資料在核心的記憶體)

4、作業系統把資料傳輸到磁碟控制器(資料此時在磁碟快取中)

5、磁碟控制器執行真正寫入資料到物理媒介的操作(如磁碟)

如果只是考慮資料庫層面,資料在第三階段之後就安全了,在這個時候,系統呼叫已經發起了,即使資料庫程式奔潰了,系統呼叫會繼續進行,也能順利將資料寫入到磁碟中。 在這一步之後,在第4步核心會將資料從核心快取儲存到磁碟快取中,但為了系統的效率問題,預設情況下不會太頻繁地執行這個動作,大概會在30s執行一次,這就意味著如果這一步失敗了或者就在進行這一步的時候伺服器突然關機了,那麼就可能會有30s的資料丟失了,這種比較普通的災難性問題也是需要考慮的。

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的持久化方案也不是一成不變的,紙上的理論還需要結合實踐成果來證明其可行性。

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

更多精彩內容,請關注個人公眾號。

[Redis原始碼閱讀]redis持久化

參考文章: oldblog.antirez.com/post/redis-… blog.httrack.com/blog/2013/1…

相關文章