1. 程式人生 > >Redis 中的資料持久化策略(RDB)

Redis 中的資料持久化策略(RDB)

Redis 是一個記憶體資料庫,所有的資料都直接儲存在記憶體中,那麼,一旦 Redis 程序異常退出,或伺服器本身異常宕機,我們儲存在 Redis 中的資料就憑空消失,再也找不到了。

Redis 作為一個優秀的資料中介軟體,必定是擁有自己的持久化資料備份機制的,redis 中主要有兩種持久化策略,用於將儲存在記憶體中的資料備份到磁碟上,並且在伺服器重啟時進行備份檔案過載。

RDB 和 AOF 是 Redis 內部的兩種資料持久化策略,這是兩種不同的持久化策略,一種是基於記憶體快照,一種是基於操作日誌,那麼本篇就先來講講 RDB 這種基於記憶體快照的持久化策略。

一、什麼是 RDB 持久化策略

RDB(redis database),快照持久化策略。RDB 是 redis 預設的持久化策略,你可以開啟 redis.conf,預設會看到這三條配置。

save 900 1            900秒內執行一次set操作 則持久化1次  
save 300 10           300秒內執行10次set操作,則持久化1次
save 60 10000         60秒內執行10000次set操作,則持久化1次

RDB 又分為兩種,一種是同步的,呼叫 save 命令即可觸發 redis 進行 RDB 檔案生成備份,但是這是一個同步命令,在備份完成之前,redis 伺服器不響應客戶端任何請求。另一種是非同步的,呼叫 bgsave 命令,redis 伺服器 fork 一個子程序進行 RDB 檔案備份生成,與此同時,主程序依然可以響應客戶端請求。

顯然,非同步的 RDB 生成策略才是主流,除了某些特殊情況,相信不會有人會在生產環境中用 save 命令阻塞 redis 服務來生成 RDB 檔案的。

以上我們介紹的兩個命令,save 和 bgsave,這兩個命令需要我們手動的在客戶端傳送請求才能觸發,我們叫做主動觸發。

而我們之前匆匆介紹過的配置觸發,這種我們叫做被動觸發,被動觸發有一些配置,下面我們來看看。

1、save 配置

save 配置是一個非常重要的配置,它配置了 redis 伺服器在什麼情況下自動觸發 bgsave 非同步 RDB 備份檔案生成。

基本語法格式:

save <seconds> <changes>

當 redis 資料庫在

2、dbfilename 配置

dbfilename 配置項決定了生成的 RDB 檔名稱,預設配置為 dump.rdb。

dbfilename dump.rdb

3、rdbcompression 配置

rdbcompression 配置的是 rdb 檔案中壓縮啟用配置,基本語法格式:

rdbcompression yes(|no)

如果 rdbcompression 配置為 yes,那麼即代表 redis 進行 RDB 檔案生成中,如果遇到字串物件並且其中的字串值佔用超過 20 個位元組,那麼就會對字串進行 LZF 演算法進行壓縮。

4、stop-writes-on-bgsave-error 配置

stop-writes-on-bgsave-error 配置了,如果進行 RDB 備份檔案生成過程中,遭遇錯誤,是否停止 redis 提供寫服務,以警示使用者 RDB 備份異常,預設是開啟狀態。

stop-writes-on-bgsave-error yes(|no)

5、dir 配置

dir 配置的是 rdb 檔案存放的目錄,預設是當前目錄。

dir ./

6、rdbchecksum 配置

rdbchecksum 配置 redis 是否使用 CRC64 校驗演算法校驗 RDB 檔案是否發生損壞,預設開啟狀態,如果你需要提升效能,可以選擇性關閉。

rdbchecksum yes(|no)

二、saveparams 和 dirty 計數器

我們 redisServer 結構體中有這麼兩個欄位:

saveparams 結構定義如下:

struct saveparam {
    time_t seconds;  //秒數
    int changes;    //變更次數
};

相信你能夠想到,上述配置檔案中的 save 配置就對應了兩個引數,多少秒內資料庫發生了多少次的變更便觸發 bgsave。

對映到程式碼就是我們 saveparam 結構,每一個 saveparam 結構都對應一行 save 配置,而最終會以 saveparam 陣列的形式被讀取到 redisServer 中。

ps:介紹這個的目前是為我們稍後分析 RDB 檔案生成的原始碼實現做前置鋪墊。

除此之外,redisServer 資料結構中還有這麼兩個欄位:

dirty 欄位記錄了自上次成功備份 RDB 檔案之後,包括 save 和 bgsave 命令,整個 redis 資料庫又發生了多少次修改。dirty_before_bgsave 欄位可以理解為上一次 bgsave 命令備份時,資料庫總的修改次數。

還有一些跟持久化相關時間欄位,上一次成功 RDB 備份的時間點,上一次 bgsave 命令開始執行時間等等。

下面我們也貼上貼上原始碼,分析分析看 redis 是如何進行 RDB 備份檔案生成的。

int serverCron(....){
    .....
    //如果已經有子程序在執行 RDB 生成,或者 AOF 恢復,或者有子程序未返回
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        int statloc;
        pid_t pid;
        //檢視這個程序是否返回訊號
        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;

            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
            //持久化異常,列印日誌
            if (pid == -1) {
                serverLog(LL_WARNING,"wait3() returned an error: %s. "
                    "rdb_child_pid = %d, aof_child_pid = %d",
                    strerror(errno),
                    (int) server.rdb_child_pid,
                    (int) server.aof_child_pid);
            } else if (pid == server.rdb_child_pid) {
                //成功持久化 RDB 檔案,呼叫方法用心的RDB檔案覆蓋舊的RDB檔案
                backgroundSaveDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
            } else if (pid == server.aof_child_pid) {
                //成功執行 AOF,替換現有的 AOF檔案
                backgroundRewriteDoneHandler(exitcode,bysignal);
                if (!bysignal && exitcode == 0) receiveChildInfo();
            } else {
                //子程序成功,但返回的 pid 型別異常,無法匹配
                if (!ldbRemoveChild(pid)) {
                    serverLog(LL_WARNING,
                        "Warning, detected child with unmatched pid: %ld",
                        (long)pid);
                }
            }
            //如果子程序未結束,不允許字典進行 rehash
            updateDictResizePolicy();
            closeChildInfoPipe();
        }
    } else{.......}
}

serverCron 每隔一百毫秒執行一次(可能後續的 redis 版本有所區別,本文基於 4.0),都會首先去判斷 RDB 或 AOF 子程序是否成功完成,如果成功會進行舊檔案替換覆蓋操作等。我們繼續看 else 部分。

int serverCron(....){
    .....
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        ..........
    }
    else{
        //如果未有子程序做 RDB 檔案生成
        //遍歷 saveparams 陣列,取出我們配置檔案中的 save 配置項
        for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;
            //根據我們之前介紹的 dirty 計數器判斷 save 配置條件是否滿足
            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);
                //核心方法,進行 RDB 檔案生成
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
         }
         //AOF 下篇我們在介紹,本篇看 RDB
         if (server.aof_state == AOF_ON &&
             server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             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();
            }
         }
    }
}

如果未有子程序進行 RDB 檔案生成,那麼遍歷迴圈我們的 save 配置項是否滿足,如果滿足則呼叫 rdbSaveBackground 進行真正的 RDB 檔案生成。我們繼續看看這個核心方法:

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;

    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;

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

    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty(-1);

            if (private_dirty) {
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }

            server.child_info_data.cow_size = private_dirty;
            sendChildInfo(CHILD_INFO_TYPE_RDB);
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
            closeChildInfoPipe();
            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 %d",childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy();
        return C_OK;
    }
    return C_OK; 
}

rdbSaveBackground 核心的是 fork 函式和 rdbSave 函式的呼叫。fork 函式其實是一個系統呼叫,他會複製出一個子程序出來,子程序和父程序幾乎一模一樣的記憶體資料。

fork 函式是阻塞的,當子程序複製出來後,程式的後續程式碼段會由父子程序同時執行,也就是說,fork 之後,接下來的程式碼,父子程序會併發執行,但系統不保證執行順序。

父程序中,fork 函式返回值等於子程序的程序 id,子程序中 fork 函式返回值等於零。

所以,rdbSaveBackground 函式的核心邏輯也就很清晰了,fork 成功之後,子程序呼叫 rdbSave 進行 RDB 檔案寫入,併產生一個“temp-%d.rdb”的臨時檔案,而父程序記錄一些日誌資訊、子程序程序號,時間等資訊。

至於 rdbSave 函式是怎麼寫入 RDB 檔案的,這個也很簡單,RDB 檔案是有固定的協議規範的,程式只要按照協議寫入資料即可,關於這個協議,我們等下詳細說它。

總結一下,serverCron 這個定期執行的函式,會將配置檔案中的 save 配置進行讀取,並判斷條件是否滿足,如果條件滿足則呼叫 rdbSaveBackground 函式 fork 出一個子程序完成 RDB 檔案的寫入,生成臨時檔案,並確保臨時檔案寫入成功後,再替換舊 RDB 檔案,最後退出子程序。

ps:fork 函式複製出來的子程序一定要記得退出,否則每一次主程序都會複製一個子程序,最終導致服務 OOM。

RDB 檔案結構分析

任何格式的檔案都會有自己的編碼協議,Java 中的位元組碼也好、圖片格式檔案也好,我們這裡的 RDB 檔案也好,都是有自己的一套約定好的協議的,具體到每一個位元組位置該放什麼樣的欄位資料,這都是約定俗成的,編碼的時候按協議寫入二進位制,讀取的時候也按照協議讀取欄位位元組。

RDB 協議規定整個檔案包括如下幾個欄位:

其中,第一部分是固定的五個位元組,redis 把它稱為 Magic Number,固定的五個字元 “R”,“E”,“D”,“I”,“S”。

我們在 redis 的 0 號資料庫中新增一個鍵值對,然後執行 save 命令生成 RDB 檔案,接著開啟這個二進位制檔案。

我們用 od 命令,並以 ASCII 碼選項輸出二進位制檔案,你會發現前五個位元組是我們固定的 redis 這五個字元。

下一個欄位 REDIS_VERSION 佔四個位元組,描述當前 RDB 的版本,以上述為例,redis-4.0 版本對應的 RDB 檔案版本就是 0008。

下一個欄位是 Aux Fields,官方稱輔助欄位,是 RDB 7 以後加入的,主要包含以下這些欄位資訊:

  1. redis-ver:版本號
  2. redis-bits:OS Arch
  3. ctime:RDB檔案建立時間
  4. used-mem:使用記憶體大小
  5. repl-stream-db:在server.master客戶端中選擇的資料庫
  6. repl-id:當前例項 replication ID
  7. repl-offset:當前例項複製的偏移量

接著就是 DATABASE 部分,這部分會儲存的我們字典中的真實資料,redis 中多個數據庫,生成 RDB 檔案的時候只會對有資料的資料庫進行寫入,而這部分的格式如下:

對應到我們上述例子中,就是這一部分:

我們的 rdb.h 檔案頭中有這麼一些常量的定義:

#define RDB_OPCODE_AUX        250
#define RDB_OPCODE_RESIZEDB   251
#define RDB_OPCODE_EXPIRETIME_MS 252
#define RDB_OPCODE_EXPIRETIME 253
#define RDB_OPCODE_SELECTDB   254
#define RDB_OPCODE_EOF        255

十六進位制 fe 轉換成十進位制就是 254,對應的就是 RDB_OPCODE_SELECTDB,標識即將開啟某資料庫,所以其後跟著的就是即將要開啟的資料庫編號,我們這裡是零號資料庫。

十六進位制 fb 轉換成十進位制就是 251,對應的就是 RDB_OPCODE_RESIZEDB,標識當前資料庫容量,即有多少個鍵,我們這裡只有一個鍵。

緊接著就是存我們的鍵值對,這部分的格式如下:

type 佔一個位元組標識當前鍵值對的型別,即物件型別,有如下可選型別:

#define RDB_TYPE_STRING 0
#define RDB_TYPE_LIST   1
#define RDB_TYPE_SET    2
#define RDB_TYPE_ZSET   3
#define RDB_TYPE_HASH   4
#define RDB_TYPE_ZSET_2 5 
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 
/* Object types for encoded objects. */
#define RDB_TYPE_HASH_ZIPMAP    9
#define RDB_TYPE_LIST_ZIPLIST  10
#define RDB_TYPE_SET_INTSET    11
#define RDB_TYPE_ZSET_ZIPLIST  12
#define RDB_TYPE_HASH_ZIPLIST  13
#define RDB_TYPE_LIST_QUICKLIST 14

key 始終是字串,由字串長度字首加上自身內容構成,後跟 value 的內容。

EOF 欄位標識 RDB 檔案的結尾,佔一個位元組,並固定值等於 255 也就是十六進位制 ff,這是能從 rdb.h 檔案頭中找到的。

CHECK_SUM 欄位儲存的是 RDB 檔案的校驗和,佔八個位元組,用於校驗 RDB 檔案是否損壞。

以上,我們就簡單介紹了 RDB 檔案的構成,其實也只是點到為止啊,每一種型別的物件進行編碼的時候都是不一樣的,還要一些壓縮物件的手法等等等等,我們這裡也不可能全部詳盡。

總的來說,對 RDB 檔案構成有個基本瞭解就行,實際上也很少有人沒事去分析 RDB 檔案裡的資料的,即便是有也是通過工具進行分析的,比如 rdb-tools 等,人工分析也太炸裂了。

好了,關於 RDB 我們就簡單介紹到這,下一篇我們研究研究 AOF 這種持久化策略,再見!


關注公眾不迷路,一個愛分享的程式設計師。

公眾號回覆「1024」加作者微信一起探討學習!

每篇文章用到的所有案例程式碼素材都會上傳我個人 github

https://github.com/SingleYam/overview_java

歡迎來踩!

相關推薦

Redis 資料持久化策略RDB

Redis 是一個記憶體資料庫,所有的資料都直接儲存在記憶體中,那麼,一旦 Redis 程序異常退出,或伺服器本身異常宕機,我們儲存在 Redis 中的資料就憑空消失,再也找不到了。 Redis 作為一個優秀的資料中介軟體,必定是擁有自己的持久化資料備份機制的,redis 中主要有兩種持久化策略,用於將儲存在

Redis 資料持久化策略AOF

上一篇文章,我們講的是 Redis 的一種基於記憶體快照的持久化儲存策略 RDB,本質上他就是讓 redis fork 出一個子程序遍歷我們所有資料庫中的字典,進行磁碟檔案的寫入。 但其實這種方式是有缺點的,先不說阻塞式 save 呼叫會阻塞整個 redis 服務,即便非同步式 bgsave 也是基於時間間隔

Redis持久化策略RDB &AOF redis持久化的幾種方式 1、前言

redis持久化的幾種方式 1、前言 Redis是一種高階key-value資料庫。它跟memcached類似,不過資料可以持久化,而且支援的資料型別很豐富。有字串,連結串列,集 合和有序集合。支援在伺服器端計算集合的並,交和補集(difference)等,還支援多種

Redis提供的持久化機制RDB和AOF

來源:https://www.cnblogs.com/xingzc/p/5988080.html   Redis是一種面向“key-value”型別資料的分散式NoSQL資料庫系統,具有高效能、持久儲存、適應高併發應用場景等優勢。它雖然起步較晚,但發展卻十分迅速。  近日,Redis的

倒置線性表資料的順序c++

倒置線性表中資料的順序(c++) 給LList類實現新增一個成員函式,倒置線性表中資料的順序,且演算法的執行時間為O(n) template<typename E> void LList<E>::reverse(){ Link<E> * p,*

eos原始碼賞析十三:EOS智慧合約資料持久化儲存

前面的文章(eos原始碼賞析(十):EOS智慧合約入門之區塊上鍊)中提到了fork_db,區塊生產之後會將區塊的狀態資訊等儲存在fork_db中,但是當這個動作完成之後,fork_db中的內容就會變化,用來儲存下一個區塊的狀態資訊,並不能實現對歷史區塊資訊的儲存。對於區塊鏈來

qt資料儲存方法介面的思路應用1thinkvd開發日誌

  <qt中資料儲存方法(介面)的思路>個人最早釋出在qtcn bbs http://www.qtcn.org/bbs/read.php?tid=32483中,可能由於比較理論化而讓人感覺其實際應用意義,今後其有相關的應用會逐步寫出來。     關於載入視訊檔案後

Spark 入門之十二:再看Spark的排程策略Standlone

資源排程是Spark中比較重要的內容,對排程的相關原理以及策略的瞭解對叢集的執行以及優化都會有很大的幫助,資源排程的方式有多種,Local,Standlone,Yarn,Mesos等,本文只針對Standlone的方式做簡介 幾個重要的概念 開始文章之前

Redis——持久化機制RDB和AOF

      redis將所有資料儲存在記憶體中,為了記錄資料和操作需要持久化機制儲存到磁碟。redis中用到了兩種方式:RDB和AOF。持久化有什麼用?重啟後進行資料恢復。redis在進行資料恢復的時候都會讀取rdb或者aof檔案,將資料重新載入

Redis持久化配置rdb,aof

dbsize top 需要 dir 選項 com 存在 db文件 ESS Redis的持久化有2種方式 1快照 2是日誌 Rdb快照的配置選項(內存中的快照寫入磁盤速度更快) 配置文件:save 900 1 // 900秒內,有1條寫入,則產生快照 save

進階的Redis資料持久化RDB與AOF

大家都知道,Redis之所以效能好,讀寫快,是因為Redis是一個記憶體資料庫,它的操作都幾乎基於記憶體。但是記憶體型資料庫有一個很大的弊端,就是當資料庫程序崩潰或系統重啟的時候,如果記憶體資料不儲存的話,裡面的資料就會丟失不見了。這樣的資料庫並不是一個可靠的資料庫。 所以資料的持久化是記憶體型資料庫的重中

資料之Spark--- Dependency依賴,啟動模式,shuffle,RDD持久化,變數傳遞,共享變數,分散式計算PI的值

一、Dependency:依賴:RDD分割槽之間的依存關係 --------------------------------------------------------- 1.NarrowDependency: 子RDD的每個分割槽依賴於父RDD的少量分割槽。 |

資料之scala --- 對映,元組,簡單類,內部類,物件Object,Idea安裝scala外掛,trait特質[介面],包和包的匯入

一、對映<Map> ----------------------------------------------------- 1.建立一個不可變的對映Map<k,v> ==> Map(k -> v) scala> val map

redis資料持久化

redis中資料的持久化分為兩種:1.rdb,2.aof 1.rdb   在指定的時間內將記憶體中的資料集快照寫入磁碟,恢復時,直接將快照檔案讀取到記憶體中。   rdb儲存的是dump.rdb檔案(預設)。   在redis配置檔案中可以設定儲存的檔名稱,以及每次更新檔案的間隔時間 如果在開發過程

Android 資料持久化技術資料儲存方式

在討論資料持久化技術之前我們先了解幾個概念? 什麼是瞬時資料:儲存在記憶體當中,有可能會因為程式的關閉或其他原因導致記憶體被收回而丟失的資料。   為什麼採用資料持久化技術:為了保證關鍵資料在程式退出時不被丟失。   什麼是資料持久化技術:將記憶體中的瞬時資料

八折進行 | 2018 中國大資料技術大會BDTC首輪講師陣容震撼來襲!

暌違一載,今又相約。作為年度技術趨勢與行業應用的風向標,2018 中國大資料技術大會(BDTC 2018)攜主題“大資料新應用”再度強勢來襲,穩踏技術時代浪潮,勢將引爆今冬技術圈。 2018 年12 月 6-8 日,由中國計算機學會主辦,CCF大資料專家委員會承辦,CS

python資料型別——集合set

集合的定義 In [1]: s2 = {} In [2]: type(s2)

python資料型別——列表list

數值型別:int (long) float 布林型 字串 列表(list) python2中(int long): python3中(int): 列表的定義 C語言中陣列:儲存同一種資料型別的集和 scores=[1,2,33,44] 列表(打了激素的陣列):可以

php資料的儲存主要講述的檔案的儲存

程式語言中對資料劃分了很多的型別,但資料與之相關的有兩個方面:其一是值,其二是型別。 程式在運算的過程中,會產生資料,但程式執行結束記憶體中的資料都會丟失。如果想儲存程式執行過程中產生資料,要儲存起來。儲存的位置文字檔案或資料庫。 但是文字檔案中只能儲存字元資訊。為了將資料的資料與型別一

python資料型別——字典dict

字典的建立 字典 key-value 鍵值對儲存的一種資料結構 value值可以是任意資料型別:int float long list tuple set dict 定義一個空字典 In [1]: d = {} In [2]: type(d)