
去年有次線上值班早上六點被人從被窩里叫醒現(xiàn)象是“Redis重啟后數(shù)據(jù)全部消失”。當時查了一圈不是主從切換、不是內(nèi)存淘汰、也不是誰手滑執(zhí)行了 FLUSHALL最后才發(fā)現(xiàn)是新部署的節(jié)點上持久化根本沒開啟。從那以后我養(yǎng)成了一個習慣任何 Redis 節(jié)點上線第一件事先確認 RDB 和 AOF 到底開沒開。這篇文章說的正是 Redis 持久化的兩條主流路線——RDB 快照和 AOF 日志。RDB 是在固定時間點給全量數(shù)據(jù)拍一張二進制照片AOF 則是把每次寫命令按 Redis 協(xié)議格式追加進日志。這篇內(nèi)容不只講它倆的區(qū)別也把原理、配置、真實環(huán)境里容易踩的坑一起過一遍適合剛?cè)腴T想系統(tǒng)理解持久化的同學(xué)也適合被“重啟后數(shù)據(jù)全沒了”折磨過的后端和運維朋友。1. RDB與AOF的核心定位與整體設(shè)計思路1.1 為什么Redis要同時維護兩套持久化方案先理清背景Redis 的數(shù)據(jù)默認都活在內(nèi)存里讀寫性能極好但代價是進程退出、主機宕機、斷電都可能導(dǎo)致數(shù)據(jù)全部蒸發(fā)。早期 Redis 只提供 RDB 持久化把某個時間點的完整數(shù)據(jù)集導(dǎo)出成一份二進制文件。RDB 的優(yōu)點是文件緊湊、恢復(fù)速度快適合做冷備和快速回滾缺點是快照之間有固定間隔比如你配置 60 秒存一次那最后 60 秒內(nèi)的全部寫入在極端情況下說丟就丟。很多業(yè)務(wù)受不了這個丟失窗口于是后來引入了 AOF。AOF 的思路和數(shù)據(jù)庫常見的 WAL 類似不保存“某個時刻的數(shù)據(jù)長什么樣”而是把每一條寫命令本身記錄下來重啟時把命令一條條重放從而重建最終數(shù)據(jù)。你可以用拍照和記賬來區(qū)分RDB 是每隔一段時間拍一張全家福AOF 是每個動作都寫進賬本。兩種方案的定位完全不同RDB 更適合備份、主從同步初始化、快速恢復(fù)AOF 更適合把數(shù)據(jù)丟失窗口壓到秒級甚至單條命令。實際生產(chǎn)里它們不是“二選一”的競爭關(guān)系而是互補關(guān)系我后面會說具體怎么組合。1.2 RDB和AOF的關(guān)鍵差異一眼看明白先給一張對比表把最核心的區(qū)別擺出來。對比項RDB 快照AOF 日志數(shù)據(jù)形態(tài)二進制快照文件內(nèi)容是序列化后的全量數(shù)據(jù)集Redis 協(xié)議格式的寫命令序列文本可讀生成方式定期、手動執(zhí)行 SAVE/BGSAVE 生成每次寫命令進入內(nèi)存緩沖區(qū)再按策略寫入磁盤恢復(fù)速度快直接加載數(shù)據(jù)文件相對慢啟動時要逐條重放命令數(shù)據(jù)安全性可能丟失上一次快照后的全部改動取決于 appendfsync 策略最小可做到每命令同步文件體積緊湊、明顯更小通常較大但可定期重寫壓縮對正常讀寫影響B(tài)GSAVE 通過 fork 子進程影響較小everysec 影響小always 寫入開銷大典型場景冷備、快速恢復(fù)、主從全量同步數(shù)據(jù)安全要求高的主實例這里有個很多文檔寫得不清楚的細節(jié)當 RDB 和 AOF 同時開啟時Redis 啟動會優(yōu)先加載 AOF而不是 RDB。原因很簡單AOF 的記錄粒度更細理論上包含更多最新數(shù)據(jù)Redis 自然選擇“信息更全”的那份文件。所以雙開時AOF 文件必須納入備份體系別以為有 RDB 兜底就不用管 AOF。2. 核心細節(jié)解析快照與日志的底層機制2.1 RDB快照的生成過程與寫時復(fù)制原理RDB 生成的核心機制可以概括為 fork 寫時復(fù)制Copy On WriteCOW。主進程執(zhí)行 BGSAVE 時會通過 fork 創(chuàng)建一個子進程子進程繼承父進程的頁表然后子進程負責把內(nèi)存里的數(shù)據(jù)集寫入臨時 RDB 文件寫完后通過 rename 原子替換成最終文件名。這樣即使生成過程崩潰也不會留下一個寫了一半的臟文件直接被加載。關(guān)鍵點是fork 出來的子進程看到的內(nèi)存頁是 fork 那一刻的快照。之后主進程仍然繼續(xù)接受新的讀寫命令如果這些新寫入修改了某個內(nèi)存頁操作系統(tǒng)會把這個頁復(fù)制一份給父進程自己用子進程繼續(xù)使用原來的舊頁。所以快照本質(zhì)上是一個“一致性視圖”不阻塞主流程。這也是為什么 BGSAVE 對大實例看起來“不卡”但實際上有兩處隱藏代價一是 fork 瞬間要復(fù)制頁表實例越大阻塞時間相對越長雖然通常只有幾毫秒到幾十毫秒二是寫時復(fù)制期間如果寫入量很大內(nèi)存頁會被大量復(fù)制極端情況下內(nèi)存使用量可能接近原有數(shù)據(jù)的兩倍內(nèi)存小的機器容易被 OOM。Linux 上還建議把vm.overcommit_memory設(shè)置為 1不然 fork 可能因為內(nèi)存預(yù)分配策略失敗。除了手動執(zhí)行 SAVE 和 BGSAVERDB 常見的觸發(fā)場景包括滿足配置的快照條件、主從全量同步、執(zhí)行 SHUTDOWN 或 DEBUG RELOAD。注意 SAVE 是同步阻塞的生產(chǎn)環(huán)境幾乎不用BGSAVE 是異步的只在 fork 瞬間阻塞。自動快照條件由save參數(shù)控制比如下面三行配置表示900 秒內(nèi)有 1 次寫操作、300 秒內(nèi)有 10 次寫操作、60 秒內(nèi)有 10000 次寫操作滿足任意一個就觸發(fā) BGSAVE。多行配置是“或”的關(guān)系條件設(shè)得越密集數(shù)據(jù)丟失窗口越小但磁盤寫壓力和 fork 頻率也越高需要自己權(quán)衡。2.2 AOF日志的寫入鏈路、刷盤策略與自動重寫AOF 的寫入鏈路可以拆成四步Redis 執(zhí)行寫命令后把命令內(nèi)容按協(xié)議格式追加到內(nèi)存里的aof_buf在合適的事件循環(huán)時機調(diào)用write()把數(shù)據(jù)寫入操作系統(tǒng)的內(nèi)核緩沖區(qū)真正的落盤由fsync控制fsync的執(zhí)行頻率由appendfsync參數(shù)決定。很多人把write()當成落盤其實不是write()只是把數(shù)據(jù)交到內(nèi)核手里斷電時內(nèi)核緩沖區(qū)的數(shù)據(jù)照樣會丟。appendfsync有三種策略區(qū)別非常明顯always每條寫命令都執(zhí)行fsync安全級別最高最多可能丟一條命令但寫入吞吐會明顯下降機械盤或者網(wǎng)絡(luò)存儲上尤其夸張。everysec由后臺線程每秒執(zhí)行一次fsync極端情況下最多丟最后一秒數(shù)據(jù)這是生產(chǎn)環(huán)境最常見的折中方案。no不主動fsync完全交給系統(tǒng)內(nèi)核自行刷盤性能最好但崩潰時可能丟更多數(shù)據(jù)。AOF 文件還有一個繞不開的問題會一直膨脹。比如對一個 key 執(zhí)行一萬次 INCRAOF 里就會有一萬條 INCR 記錄真正恢復(fù)時只需要一條 SET 就能表達最終結(jié)果。于是 Redis 提供了重寫機制BGREWRITEAOF會 fork 出一個子進程掃描當前內(nèi)存中的完整數(shù)據(jù)集生成最小的命令序列寫入臨時 AOF 文件重寫期間產(chǎn)生的新命令會被放進重寫緩沖區(qū)最后一起追加并原子替換舊文件。自動重寫的觸發(fā)條件由auto-aof-rewrite-percentage 100和auto-aof-rewrite-min-size 64mb控制意思是 AOF 體積比上次重寫后增長了 100%且絕對大小超過 64MB 時才會重寫。這個設(shè)計是為了避免頻繁重寫浪費資源。2.3 數(shù)據(jù)安全、恢復(fù)速度與混合持久化的取舍從機制上就不難理解RDB 和 AOF 的優(yōu)勢是互斥的RDB 恢復(fù)快、文件小但丟失窗口取決于快照間隔AOF 能精確到秒級甚至命令級但文件大、重放慢。為了同時拿到兩邊的優(yōu)點Redis 4.0 引入了混合持久化。開啟aof-use-rdb-preamble yes后AOF 重寫生成的日志文件前面會直接以 RDB 二進制格式寫入當前全量數(shù)據(jù)后面再追加重寫期間產(chǎn)生的少量增量命令。加載的時候先快速加載 RDB 部分再重放少量命令數(shù)據(jù)安全性和恢復(fù)速度都有提升。判斷一個 AOF 文件是不是混合格式很簡單用head -c 5 appendonly.aof查看如果開頭是REDIS字樣就說明帶 RDB 前綴。Redis 5、6、7 的默認配置里基本都開了這個選項所以你在生產(chǎn)環(huán)境看到的大多數(shù) AOF 文件已經(jīng)不是純文本命令了沒必要驚訝。3. 實操過程配置、選型與備份恢復(fù)3.1 RDB配置參數(shù)怎么調(diào)才不出錯先給一份可以直接參考的 RDB 配置片段# 自動快照觸發(fā)條件滿足任一條件就執(zhí)行 BGSAVE save 3600 1 save 300 100 save 60 10000 # 快照文件名和目錄 dbfilename dump.rdb dir /var/lib/redis # BGSAVE 失敗時是否停止寫入 stop-writes-on-bgsave-error yes # 快照文件是否壓縮、是否做 CRC64 校驗 rdbcompression yes rdbchecksum yesdir這個參數(shù)特別容易被忽略。很多人只在配置里寫了dbfilename卻沒注意 Redis 的實際工作目錄導(dǎo)致啟動后找不到之前的dump.rdb。用 systemd 啟動時工作目錄可能是/或/var/lib/redis和你手動執(zhí)行 redis-server 的目錄不一定一樣。建議每次部署后執(zhí)行config get dir確認路徑。stop-writes-on-bgsave-error yes的意思是磁盤出現(xiàn)故障、快照無法生成時Redis 會主動拒絕寫命令這個行為乍看有點激進但能避免“客戶端以為寫入成功、重啟后數(shù)據(jù)全沒”的惡性事故線上環(huán)境建議保持開啟。rdbchecksum會帶來少量讀寫開銷但能識別文件損壞除非你對性能極度敏感否則不建議關(guān)閉。判斷 RDB 是否正??梢杂胕nfo persistence看rdb_last_bgsave_status和rdb_bgsave_in_progress。如果你看到rdb_last_bgsave_status:err那就要立刻查磁盤因為說明上一次快照已經(jīng)失敗了。3.2 AOF配置參數(shù)與混合持久化的正確開啟方式AOF 相關(guān)配置我一般這樣寫appendonly yes appendfilename appendonly.aof appendfsync everysec # 重寫期間是否照常 fsync no-appendfsync-on-rewrite no # 自動重寫觸發(fā)條件 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 允許加載截斷的 AOF 文件 aof-load-truncated yes # 混合持久化 aof-use-rdb-preamble yesappendfsync我?guī)缀醪粫谏a(chǎn)環(huán)境開always除非業(yè)務(wù)對數(shù)據(jù)一致性有硬性要求。因為always對吞吐影響太大了在大量寫入場景下會直接把 Redis 的寫性能拉低一個量級。everysec丟一秒數(shù)據(jù)的窗口對絕大多數(shù)業(yè)務(wù)都可以接受這也是 Redis 官方文檔里推薦的折中。no-appendfsync-on-rewrite控制的是重寫期間是否暫停fsync默認no會有更好的安全性但重寫時磁盤壓力變大。如果你的寫入量很大AOF 重寫又頻繁可以考慮臨時調(diào)整。還有一個經(jīng)??尤说膱鼍皬摹爸婚_ RDB”切換到“開啟 AOF”。很多人用config set appendonly yes在線開啟看到 AOF 文件生成了就以為完事結(jié)果重啟后配置又回到no最后還是加載 RDB甚至因為 RDB 沒更新而丟數(shù)據(jù)。正確做法是熱切換后立刻執(zhí)行config rewrite把運行參數(shù)固化到配置文件里同時手工檢查redis.conf里確實已經(jīng)變成appendonly yes。熱切換本身不會丟失原有數(shù)據(jù)因為 Redis 會基于當前內(nèi)存狀態(tài)生成初始 AOF 基底關(guān)鍵是后續(xù)重啟時配置不能漂。3.3 不同業(yè)務(wù)場景下RDB和AOF怎么選型選型沒有一個萬能答案只能按業(yè)務(wù)對數(shù)據(jù)丟失的容忍度來分。業(yè)務(wù)場景持久化建議原因純緩存緩存可重建可完全不持久化或只開 RDB丟數(shù)據(jù)影響小追求性能和運維簡單登錄態(tài)、用戶會話、購物車AOF everysec RDB允許極小概率丟一秒但重啟能快速恢復(fù)訂單、積分、分布式鎖AOF everysec 或 always RDB丟失記錄會造成資損或并發(fā)安全問題需更高防護從節(jié)點、只讀副本通常關(guān)閉持久化或只開 RDB數(shù)據(jù)由主節(jié)點下發(fā)重點承擔讀流量容災(zāi)冷備周期性 RDB 文件異地備份RDB 文件小、加載快適合歸檔特別提醒一點使用 Redis 做分布式鎖時持久化配置不能想當然。如果主庫重啟導(dǎo)致鎖記錄丟失所有客戶端可能同時認為自己持有鎖進而產(chǎn)生并發(fā)安全問題。所以鎖服務(wù)所在實例建議至少開啟 AOF everysec條件允許時再配合主從和哨兵。這也是現(xiàn)在很多 Redis 面試題不斷追問持久化的原因它直接關(guān)系到分布式環(huán)境下的一致性表現(xiàn)。容器環(huán)境里還有三個額外的坑要避開數(shù)據(jù)目錄必須掛載到持久化卷否則容器重建等于銷毀文件啟動命令里要顯式指定redis.conf很多鏡像默認不加載外部配置停止容器時不要用強殺要留足夠?qū)捪奁谧?Redis 正常退出并處理關(guān)閉流程。3.4 一份可直接落地的持久化恢復(fù)流程這里給一套我實際用過的恢復(fù)流程操作順序非常關(guān)鍵先停掉業(yè)務(wù)寫入或者直接下線實例避免恢復(fù)過程中產(chǎn)生臟數(shù)據(jù)。用info persistence確認當前持久化開關(guān)和文件路徑定位 RDB/AOF 文件到底在哪。先用redis-check-rdb和redis-check-aof檢查文件完整性不要直接啟動 Redis否則可能加載損壞文件后繼續(xù)工作把問題擴大。備份一份原地文件再做任何修復(fù)操作。修復(fù)損壞文件確認無誤后啟動 Redis觀察啟動日志確認加載了多少數(shù)據(jù)。用客戶端抽查幾個關(guān)鍵 key確認數(shù)據(jù)量級與預(yù)期一致再恢復(fù)業(yè)務(wù)流量。這套流程看起來平淡但能避免“啟動成功但數(shù)據(jù)不對”的二次事故。尤其檢查文件完整性這一步很多人會跳過真實生產(chǎn)里我見過 AOF 文件損壞后 Redis 拒絕啟動然后有人直接刪除 AOF 文件重啟的等于是主動選擇了從零開始。4. 常見問題與排查技巧實錄4.1 重啟后數(shù)據(jù)丟了優(yōu)先查這幾個地方“Redis 重啟后數(shù)據(jù)全丟”是我被問得最多的一個問題排查順序一般是這樣持久化開關(guān)真的開了嗎有的實例啟動命令沒有指定配置文件或者 systemd 的 ExecStart 覆蓋了配置導(dǎo)致save被注釋、appendonly no看起來是 Redis 在運行實際沒有任何持久化動作。配置文件和工作目錄對不對最常見的是dir路徑不一致Redis 啟動后找不到舊的dump.rdb或appendonly.aof自然恢復(fù)成空庫。磁盤和權(quán)限正常嗎磁盤滿了或者文件寫不進去RDB 持久化失敗后stop-writes-on-bgsave-error可能已經(jīng)讓 Redis 停止寫入了但業(yè)務(wù)側(cè)可能只看到“寫入超時”不會馬上聯(lián)想到持久化問題。容器場景掛載了嗎Docker 里沒掛數(shù)據(jù)卷容器重建后數(shù)據(jù)文件直接沒了這種情況和 Redis 本身關(guān)系不大但最容易讓新手誤判。我遇到過最隱蔽的一次是AOF 確實開了但只是config set appendonly yes在線生效配置文件一直沒更新。之后有人重啟了 Redis配置回退重啟恢復(fù)的是舊 RDB數(shù)據(jù)直接倒退了一個晚上。4.2 RDB/AOF文件損壞了怎么修Redis 啟動時如果發(fā)現(xiàn)文件損壞日志里通常會出現(xiàn)Wrong signature trying to load RDB file或Bad file format之類的信息。RDB 文件用redis-check-rdb檢查AOF 文件用redis-check-aof修復(fù)命令大概是# 檢查 RDB 文件 redis-check-rdb /var/lib/redis/dump.rdb # 修復(fù) AOF 文件--fix 會去掉尾部損壞的無效命令 redis-check-aof --fix /var/lib/redis/appendonly.aofAOF 修復(fù)的本質(zhì)是“截斷”會把無法解析的尾部命令丟掉所以修復(fù)后確實會損失崩潰瞬間附近的一點數(shù)據(jù)但至少能保住前面絕大部分內(nèi)容。執(zhí)行修復(fù)前一定先備份原文件我習慣把損壞文件復(fù)制成appendonly.aof.bak再動手。另外Redis 提供了aof-load-truncated yes配置如果 AOF 只是末尾被截斷而非中間損壞Redis 會允許加載并打日志警告這個參數(shù)默認開啟不建議關(guān)掉但要在監(jiān)控里把相關(guān)日志拉出來人工確認一下。4.3 fork耗時和AOF重寫導(dǎo)致的性能抖動怎么處理持久化影響性能主要有兩個場景BGSAVE 的 fork 阻塞以及 AOF 重寫期間的磁盤競爭。排查 fork 耗時用info stats里的latest_fork_usec這個值如果持續(xù)飆升到幾百毫秒甚至秒級說明實例很大或者機器內(nèi)存壓力很高。此時可以在業(yè)務(wù)低峰期手動執(zhí)行 BGSAVE錯開高峰也可以把 RDB 備份任務(wù)放到從節(jié)點去做主節(jié)點專心服務(wù)讀寫。AOF 重寫的自動觸發(fā)條件不好精確控制如果業(yè)務(wù)高峰時正好撞上重寫會出現(xiàn)短暫延遲。我的做法是在低峰期手動執(zhí)行BGREWRITEAOF把重寫時間控在自己可控范圍內(nèi)并把auto-aof-rewrite-percentage調(diào)大一些降低自動重寫的頻率。另外如果 RDB 和 AOF 落在同一塊磁盤上BGSAVE 和 AOF 刷盤會互相爭搶 IO條件允許時把它們分到不同物理盤或云盤上能明顯緩解抖動。4.4 版本兼容與加載順序那些容易忽略的坑RDB 文件存在版本兼容問題老版本 Redis 可能無法加載新版本生成的 RDB。升級 Redis 版本后如果日志報類似Cant handle RDB format version的錯誤說明新文件格式舊進程讀不了。升級前最好先停服手動觸發(fā)一次 BGSAVE確認能恢復(fù)再跑版本升級。AOF 命令格式相對穩(wěn)定但如果你跨大版本升級也不能完全賭它不出問題穩(wěn)妥做法是先備份 AOF 再升級。關(guān)于加載順序的坑我再強調(diào)一次同時開啟 RDB 和 AOF 時Redis 啟動優(yōu)先加載 AOF。有些同學(xué)認為“我有 RDBAOF 丟了不慌”結(jié)果 AOF 文件損壞或缺失時啟動加載可能失敗或者恢復(fù)出異常數(shù)據(jù)。所以雙開模式下AOF 文件和 RDB 一樣重要都要納入備份和巡檢范圍。4.5 面試里怎么把RDB和AOF講清楚面試題如果問到持久化一個清晰的回答結(jié)構(gòu)大概是先定義——RDB 是定期生成的二進制快照AOF 是追加式命令日志再說優(yōu)缺點——RDB 恢復(fù)快、文件小但丟失窗口大AOF 丟失窗口小但文件大、恢復(fù)慢然后提到機制——BGSAVE 依賴 fork 和寫時復(fù)制AOF 靠appendfsync控制刷盤體積膨脹靠BGREWRITEAOF解決最后說生產(chǎn)實踐——雙開開啟AOF everysec 為主RDB 做冷備和兜底Redis 4.0 之后開混合持久化啟動加載時優(yōu)先 AOF。這樣答既有深度又能落到工程實踐基本不會冷場。我個人在實際維護中的默認組合是Redis 7 AOF everysec RDB 兜底 混合持久化RDB 文件周期性異地備份。AOF 負責把崩潰丟失控制在一秒內(nèi)RDB 負責快速冷備和主從初始化兩者關(guān)掉任何一個我都會覺得心里沒底。最后再分享一個小技巧每次維護重啟前執(zhí)行一次config rewrite把當前有效參數(shù)固化到文件里再配合info persistence查看 RDB/AOF 狀態(tài)能避開不少“重啟完配置漂移”的低級問題。