快轉到主要內容
  1. 資料庫 首頁/

Redis 持久化設定:RDB 與 AOF

·4 分鐘· ·
Blog Zh-Tw Database Redis
Liu Zhe You
作者
Liu Zhe You
涉略全端、DevOps,目前專注在 Backend
目錄

Redis 持久化設定:RDB 與 AOF
#

為什麼需要 Redis 持久化?
#

因為 Redis 是一個 in-memory 的資料庫 所以當 Redis 服務重啟時,所有資料都會消失

不過可以透過持久化設定 讓 Redis 在重啟時,能夠恢復狀態 !

Redis 持久化設定
#

Redis 提供了兩種持久化設定 分別是 RDBAOF

  • RDB:Redis Database
    RDB 會在指定的間隔時間內執行對資料庫的快照 (snapshot)。
  • AOF:Append Only File
    AOF 會將伺服器接收到的每個寫入操作記錄下來。這些操作可以在伺服器啟動時重新執行,以重建原始資料集。
  • RDB+AOF:RDB 與 AOF
    可以同時啟用 RDB 與 AOF 持久化,以提供更強大的資料保護機制。

RDB: Redis Database
#

簡而言之,
RDB 需要 fork() 一個子 process 來建立資料庫的快照到磁碟上。

RDB 的優點
#

RDB 其實只是一個非常緊湊的檔案,能夠表示你在某一時間點的 Redis 快照

  • 省空間:RDB 比 AOF 更節省空間
  • 備份更簡單:RDB 更容易備份和恢復

    因為它是一個單文件,可以把它複製到任何存儲,如 S3、Google Cloud Storage 等。

  • 對於大 datasets 能夠更快恢復:與 AOF 相比,RDB 在擁有大數據集時能夠更快地恢復

RDB 的缺點
#

  • 數據丟失:如果你需要最小化數據丟失,RDB 並不是一個好的選擇

    因為它只在指定的間隔時間保存數據集

  • 不適合大數據集RDB 不適合大數據集

    因為它需要 fork() 一個子進程來將數據集保存到磁碟
    如果你有一個大的數據集,fork()很慢
    可能會停止服務客戶端數毫秒甚至數秒!

RDB 的詳細實作
#

我們提到 RDB 需要 fork() 一個子進程來將數據集保存到磁碟。

讓我們來看看 Redis 源碼中 RDB 的g6yji4。

  1. redis/src/rdb.c:定義了 bgsaveCommand 命令入口函數。

    3907
    3908
    3909
    3910
    3911
    3912
    3913
    3914
    3915
    3916
    3917
    3918
    3919
    3920
    3921
    3922
    3923
    3924
    3925
    3926
    3927
    3928
    3929
    3930
    3931
    3932
    3933
    3934
    3935
    3936
    3937
    3938
    3939
    3940
    3941
    3942
    
    /* BGSAVE [SCHEDULE] */
    void bgsaveCommand(client *c) {
        int schedule = 0;
    
        /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
         * is in progress. Instead of returning an error a BGSAVE gets scheduled. */
        if (c->argc > 1) {
            if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
                schedule = 1;
            } else {
                addReplyErrorObject(c,shared.syntaxerr);
                return;
            }
        }
    
        rdbSaveInfo rsi, *rsiptr;
        rsiptr = rdbPopulateSaveInfo(&rsi);
    
        if (server.child_type == CHILD_TYPE_RDB) {
            addReplyError(c,"Background save already in progress");
        } else if (hasActiveChildProcess() || server.in_exec) {
            if (schedule || server.in_exec) {
                server.rdb_bgsave_scheduled = 1;
                addReplyStatus(c,"Background saving scheduled");
            } else {
                addReplyError(c,
                "Another child process is active (AOF?): can't BGSAVE right now. "
                "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
                "possible.");
            }
        } else if (rdbSaveBackground(SLAVE_REQ_NONE,server.rdb_filename,rsiptr,RDBFLAGS_NONE) == C_OK) {
            addReplyStatus(c,"Background saving started");
        } else {
            addReplyErrorObject(c,shared.err);
        }
    }
    

  2. bgsaveCommand 實際上會調用 rdbSaveBackground
    rdbSaveBackground 會檢查是否已經有子 process 在運行,如果沒有,會調用 redisFork 來 fork 一個子進程將數據集保存到磁碟。

    1612
    1613
    1614
    1615
    1616
    1617
    1618
    1619
    1620
    1621
    1622
    1623
    1624
    1625
    1626
    1627
    1628
    1629
    1630
    1631
    1632
    1633
    1634
    1635
    1636
    1637
    1638
    1639
    1640
    1641
    1642
    1643
    1644
    1645
    1646
    
    int rdbSaveBackground(int req, char *filename, rdbSaveInfo *rsi, int rdbflags) {
        pid_t childpid;
    
        if (hasActiveChildProcess()) return C_ERR;
        server.stat_rdb_saves++;
    
        server.dirty_before_bgsave = server.dirty;
        server.lastbgsave_try = time(NULL);
    
        if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
            int retval;
    
            /* Child */
            redisSetProcTitle("redis-rdb-bgsave");
            redisSetCpuAffinity(server.bgsave_cpulist);
            retval = rdbSave(req, filename,rsi,rdbflags);
            if (retval == C_OK) {
                sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
            }
            exitFromChild((retval == C_OK) ? 0 : 1);
        } else {
            /* Parent */
            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 */
    }
    

由於 fork() 是一個system call ,它會將 parent process 的整個 memory複製到子進程。這就是為什麼當你有一個大數據集時,fork() 會很慢!

Redis 有使用 CoW (Copy-on-Write) 來優化 fork()

此外,如果你有一個大數據集並且剩餘記憶體有限,這可能會導致 OOM (Out of Memory)

AOF: Append Only File
#

AOF 利用 fsync() system call 將每次寫操作保存到磁碟。

就像關聯數據庫中的 WAL (Write-Ahead Logging) fsync 在後台執行,以避免阻塞 Redis 主事件循環。

AOF 有三種 fsync 策略:

  • always:每次寫操作後都進行 fsync()
  • everysec:每秒進行一次 fsync() (默認)
  • no:只有在明確調用 fsync() 時才進行

    這是更快但不太安全的方法。
    只需將數據交給操作系統處理。通常 Linux 在這種配置下會每 30 秒刷新數據,但這取決於內核的具體調整。

AOF 重寫

  • Redis 將在後台重寫 AOF 文件,避免文件變得過大。
  • serverCron 會檢查 AOF 文件是否過大,如果是,會調用 rewriteAppendOnlyFileBackground 在後台重寫 AOF 文件。

類似於 RDB,AOF 重寫 也需要 fork() 一個子進程將數據集保存到磁碟。
當你有一個大數據集時,可能會遇到與 RDB 相同的問題。

AOF 的優點
#

  • 丟失較少數據:如果你需要最小化數據丟失,AOF 更好

    因為它將每次寫操作保存到磁碟

AOF 的缺點
#

  • 文件大小較大AOF 文件通常比相應的 RDB 文件大
  • RDB 慢(取決於 fsync() 策略)
    • 默認的 everysec 策略仍然非常高效!
    • 但如果你設置 always 策略,它會比 RDB 慢,因為它需要在每次寫操作後進行 fsync()

Reference
#

相關文章

PgBouncer: 輕量 Postgres 連接池
·2 分鐘
Blog Database Zh-Tw Postgresql
以 PgBouncer 解決 Django 後端 DB connection 過多的問題
Python: 重複讀取檔案(BinaryIO)
·1 分鐘
Blog Zh-Tw Python
在 Python 中重複讀取檔案 (BinaryIO),如何解決在第二次讀取時出現空內容的問題。
Cloudflare Tunnel
·2 分鐘
Blog Zh-Tw
設定 Cloudflare Tunnel 來穿透內網 IP,Ngrok 的替代方案
在 SqlAlchemy 使用 Transaction
·2 分鐘
Blog Zh-Tw SqlAlchemy Backend Python
如何在 SqlAlchemy 中使用 Transaction
常用 tmux 指令
·2 分鐘
Blog Zh-Tw
常用 tmux 指令 Cheat Sheet
FastAPI: 使用 Moto 模擬 S3
·2 分鐘
Blog Zh-Tw AWS Backend Testing FastAPI
FastAPI 測試: 使用 Moto 模擬 AWS S3 Boto3