一文讀懂 Redis RDB 持久化:策略、配置與應用
在Redis的世界里,數據的持久化猶如堅固的基石,支撐著整個系統的穩定運行與數據安全。在實際應用場景中,我們不僅需要Redis強大的內存處理能力,更需要確保數據在各種意外情況下不丟失,這時候持久化機制就顯得尤為重要。
Redis RDB(Redis Database)持久化作為Redis重要的持久化方式之一,有著獨特的魅力與價值。它以一種緊湊且高效的方式將Redis在某一時刻的數據快照保存到磁盤上,在Redis重啟時,可以快速地將這些數據恢復到內存中,極大地提高了系統的可用性和數據恢復效率。
本文將深入探討Redis RDB持久化,從它的基本概念、工作原理講起,詳細闡述其優缺點、觸發機制、配置參數等關鍵內容。無論是初涉Redis的新手,還是想要深入理解持久化機制的開發者,都能從本文中收獲關于Redis RDB持久化的全面且深入的知識,為實際項目開發提供有力支持。
一、詳解RDB基礎
1. 什么是RDB
RDB持久化機制是將內存中的數據生成快照并持久化到磁盤的過程,RDB可以通過手動或者自動的方式實現持久化:
2. RDB的幾種觸發時機
(1) 手動觸發
我們先來說說手動觸發即save命令,這個指令會直接阻塞當前redis服務器,知道RDB完成了為止,對于線上生產環境數據的備份,我們非常非常不建議使用這種方式。
ounter(lineounter(lineounter(line
127.0.0.1:6379> save
OK
接下來就是bgsave指令了,bgsave則是主進程fork一個子進程,由子進程完成持久化操作,而主進程繼續處理客戶端的讀寫請求,如果我們需要手動實現持久化,非常推薦使用這種方式。
ounter(lineounter(lineounter(lineounter(line
# 從輸出我們就可以看出這種方式會將持久化的操作放在后臺執行
127.0.0.1:6379> bgsave
Background saving started
(2) 被動觸發
還有一種就是被動觸發,或者說是自動觸發,自動觸發我們可以通過配置實現redis.conf的save參數實現,如下所示,假如我們希望用戶20s內寫入3次就進行持久化,只需在配置中加一條save 20 3即可。
ounter(lineounter(line
save 20 3
需要注意的是save 20 3的20s是以redis的時間間隔為主,并不是用戶第1次寫入后的20s內再寫入兩次進行持久化,本質上被動觸發是由redis server的一個定時任務掃描執行:
(3) 關閉時持久化
當我們執行shutdown指令時,如果沒有明確指明參數nosave,該指令會調用rdbSave將當前內存中的鍵值對持久化到rdb文件中:
對應我們也給出redis源碼中關于shutdown持久化的核心代碼,即位于db.c的shutdownCommand函數:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
void shutdownCommand(redisClient *c) {
//......
//調用prepareForShutdown執行rdb持久化
if (prepareForShutdown(flags) == REDIS_OK) exit(0);
addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
}
//服務器進程關閉時調用rdbSave生成rdb文件
int prepareForShutdown(int flags) {
int save = flags & REDIS_SHUTDOWN_SAVE;
int nosave = flags & REDIS_SHUTDOWN_NOSAVE;
//......
//如果存在rdb子進程則殺掉
if (server.rdb_child_pid != -1) {
redisLog(REDIS_WARNING,"There is a child saving an .rdb. Killing it!");
kill(server.rdb_child_pid,SIGUSR1);
rdbRemoveTempFile(server.rdb_child_pid);
}
//......
/**
* 符合以下任意條件都會觸發rdb持久化:
* 1. 如果我們有配置save參數例如(save 20 3) 則saveparamslen大于0,且nosave非0
* 2. save為1(默認情況下會指明1)
*/
if ((server.saveparamslen > 0 && !nosave) || save) {
//執行rdb持久化
if (rdbSave(server.rdb_filename) != REDIS_OK) {
//......
return REDIS_ERR;
}
}
//......
return REDIS_OK;
}
3. RDB的使用方式
基于上述配置我們簡單演示一下RDB持久化機制,我們首先需要存點數據,20s存3個值:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> set k3 v3
OK
完成后查看是否生成rdb文件,確認無誤后,我們將這個文件備份,并強制關閉redis服務端,模擬斷電的場景:
ounter(lineounter(lineounter(line
# 重命名rdb文件
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# mv dump.rdb dump.rdb.bak
此時我們再啟動redis就會發現數據為空:
ounter(lineounter(line
127.0.0.1:6379> keys *
(empty array)
我們將rdb文件還原,并重啟redis,可以發現備份數據還原了:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
# 強制關閉redis
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# ps -ef |grep redis |grep -v grep
root 8956 1 0 23:22 ? 00:00:00 redis-server 127.0.0.1:6379
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# kill -9 8956
# 還原rdb,并啟動redis
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# mv dump.rdb.bak dump.rdb
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# redis-server /root/redis/redis.conf
[root@iZ8vb7bhe4b8nhhhpavhwpZ sbin]# redis-cli
# 可以看到之前設置的數據都回來了
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
4. 詳解bgsave的工作流程
bgsave的工作流程如下圖所示,整體可以簡述為:
- 主進程fork出一個子進程,這時候主進程會被阻塞。
- 子進程創建完成后,redis客戶端會輸出Background saving started,這就意味子進程開始進行持久化操作了。
- 子進程持久化完成后,會生成一個rdb文件,將本次的rdb文件通過原子替換的方式將上一次備份的rdb覆蓋。
- 子進程發送信號通知父進程本次任務完成。
5. RDB常見的配置參數(了解)
首先是dbfilename ,它可以指定rdb的文件名:
ounter(lineounter(lineounter(lineounter(line
# The filename where to dump the DB
dbfilename dump.rdb
接下來就是dir,它可以指定rdb文件的持久化的位置,默認取redis服務端的位置。
ounter(lineounter(linedir ./
ounter(lineounter(line
dir ./
當reids無法將文件寫入磁盤,我們可以講stop-writes-on-bgsave-error設置為yes,直接關掉redis的寫操作,默認為yes:
ounter(lineounter(lineounter(line
stop-writes-on-bgsave-error yes
rdbcompression 開啟后,redis默認會通過LZF算法壓縮rdb文件。這種方式會消耗CPU,但是壓縮后的大小遠遠小于內存,但是帶來的收益卻遠遠大于這點開銷,通過壓縮的文件無論是通過網絡發送到從節點還是存儲到硬盤的空間都是非常可觀的。
ounter(lineounter(lineounter(lineounter(line
rdbcompression yes
rdbchecksum 開啟后,在存儲快照后,還可以讓redis使用CRC64算法來進行數據校驗,但是這樣做會增加大約10%的性能消耗,如果希望獲取到最大的性能提升,可以關閉此功能。
ounter(lineounter(line
rdbchecksum yes
6. RDB有哪些優缺點
優點:
- rdb是緊湊壓縮的二進制文件,非常實用與備份或者全景復制等場景。
- rdb恢復數據效率遠遠高于aof
而缺點如下:
- 無法做到毫秒級別的實時性持久化,盡管我們可以通過設置緊湊的save完成持久化,但是頻繁的fork子進程進行持久化,很可能造成redis主進行長期阻塞。
- 存儲的文件是二進制,不夠直觀,可能還存在某些兼容問題。
二、詳解RDB進階知識點
1. 我們為Redis開辟的一塊大內存空間,進行持久化時就可能耗時長,這段時間還可能收到客戶端的請求,如何保持持久化后的數據一致性?
在進行周期性快照數據持久化期間,redis會fork一個子進程異步執行,但是父子進程仍然共享同一個代碼段和數據段,兩者并行操作存在線程安全的風險。
所以在快照持久化期間,主進程的修改操作都采用了寫時復制(Copy On Write)的思想,即將需要進行操作的鍵值對數據從原有數據頁中復制出一份副本進行修改,等到bgsave子進程快照完成后,再將這塊內存區域同步到原來的內存區域中,等待下一次快照:
這樣做的缺點也很明顯,極端情況下,如果在bgsave期間主進程數據都被改了,那么內存占用就是原來的兩倍:
2. 在進行快照操作的這段時間,如果發生服務崩潰怎么辦?
服務恢復的數據只會是上一次備份的rdb文件數據,因為bgsave子進程只會將操作成功的文件生成rdb文件覆蓋上一次備份的文件。
3. 可以每秒做一次快照嗎?
可能會有下面這幾個問題:
- 頻繁寫入內存數據會給磁盤帶來很大的壓力,多個fork子進程搶占優先的磁盤帶寬,前一個子進程沒寫完,后一個子進程又來寫入。
- 雖說快照這個操作是單位時間內只能執行一次異步,但是不間斷的rdb異步持久化每次fork子進程這個操作都會阻塞主進程,頻繁fork很可能對于性能開銷還是很大的。
- 對于全量大數據快照操作是很耗時的,即使我們延長了RDB快照的調度間隔,redis每次進行rdb持久化之前也會檢查當前是否有子進程執行快照,如果存在則不允許快照,所以針對數據量較大的場景做這種頻繁保存的操作意義也不大。
對應筆者也給出的bgsave的源碼實現,可以看到在每次進行持久化的時候bgsaveCommand都會檢查當前是否有子進程正在執行RDB持久化,如果存在則不允許用戶進行持久化:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
//調用rdbSaveBackground創建一個子進程生成rdb文件,不影響主線程
void bgsaveCommand(redisClient *c) {
//如果存在rdb或者aof的子進程則直接不允許執行bgsave
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
} else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}