這個主流大法竟是大坑!Redis 大 Key 問題怎么破?
在 Redis 運維中 ,大 Key 始終是威脅集群穩(wěn)定性的 “ 隱形炸彈”。我梳理 Redis 大 Key 解析系統(tǒng)時 ,在比較了scan查找大key方案 ,以及使用官方的redis-cli --bigkeys方案后 ,選擇 “基于 RDB的離線解析” 方案 —— 它完全不影響在線集群 ,還能獲取全量鍵的精確內(nèi)存數(shù)據(jù) ,特別適合非實時性的大 Key 排查場景(實時的可以用慢查詢系統(tǒng) ,這個以前曾在dbaplus分享過)。最近我用 Golang 實現(xiàn)了這套方案 ,今天就從場景需求、技術(shù)實現(xiàn)到實際落地 ,和大家詳細分享整個過程。
一、為什么要造這個 “輪子”?
市面上其實有現(xiàn)成的 RDB 解析工具( 比如 Python 寫的 redis-rdb-tools ),但在實際運維中,我遇到了兩個關(guān)鍵問題:
- 性能瓶頸:面對 GB 級甚至更大的 RDB 文件 ,Python 工具解析耗時過長( 曾試過解析20GB 的 RDB ,跑了近 1 小時),而運維場景中常需要批量處理多集群的 RDB 文件 ,效率亟待提升;
- 定制化不足:現(xiàn)有工具輸出的結(jié)果多是通用格式 ,無法直接對接我們內(nèi)部的運維平臺( 比如按業(yè)務(wù)線分類大 Key、 自動同步結(jié)果到 Grafana 面板),需要額外寫腳本二次處理。
考慮到 Golang 的并發(fā)優(yōu)勢和高性能特性 ,以及能直接編譯成二進制文件(方便在不同服務(wù)器部署),我決定用 Golang 從零實現(xiàn)—套 RDB 離線解析工具 ,核心目標是:快解析、可定制、易集成。
二、Golang 實現(xiàn) RDB 解析的核心思路
要解析 RDB 文件 ,首先得理解其二進制格式 ——Redis 的 RDB 文件是按特定協(xié)議存儲的二進制數(shù)據(jù) ,包含數(shù)據(jù)庫選擇、鍵值對數(shù)據(jù)、過期時間等信息。整個實現(xiàn)過程可以拆成 3 個核心步驟:
1. 準備工作:選擇合適的 RDB 解析庫
自己手寫 RDB 格式解析會耗費大量時間(要處理各種數(shù)據(jù)類型、壓縮格式),Golang 生態(tài)中有成熟的 RDB 解析庫可以復(fù)用, 選擇了 https://github.com/HDT3213/rdb (輕量、文檔清晰,支持 Redis 6.0 + 的 RDB 格式),本來打算導(dǎo)入這個庫的解析能力擴展 ,但研究發(fā)現(xiàn)這個庫不支持外部導(dǎo)出為內(nèi)部屬性 ,不能完全滿足需求 ,考慮再三 ,最后決定通過對項目進行部分魔改 ,通過replace依賴模塊指向魔改版本來處理。
2. 核心流程:從 RDB 文件到大 Key 數(shù)據(jù)
整個解析大key的流程很清晰:備份 RDB 文件 → 讀取 RDB 文件 → 解析鍵值對與內(nèi)存信息 →篩選大 Key → 輸出結(jié)果 ,下面—步步拆解關(guān)鍵代碼。
步驟 1 :讀取 RDB 文件并初始化解析器
首先要打開 RDB 文件 ,然后用 NewDecoder 初始化解析器, 同時定義—個 “結(jié)果處理器”(用來接收解析出的每—個鍵值對數(shù)據(jù))。
// 通過一個channel返回解析好的數(shù)據(jù),因為有多個rdb要解析,這里傳入一個channel統(tǒng)一接收,方便管理
func MyFindBiggestKeys(rdbFilename string, output chan<- RedisData, options ...interface{}) error {
var err error
if rdbFilename == "" {
return errors.New("src file path is required")
}
rdbFile, err := os.Open(rdbFilename)
if err != nil {
return fmt.Errorf("open rdb %s failed, %v", rdbFilename, err)
}
defer func() {
_ = rdbFile.Close()
}()
var dec decoder = core.NewDecoder(rdbFile)
if dec, err = wrapDecoder(dec, options...); err != nil {
return err
}
err = dec.Parse(func(object model.RedisObject) bool {
data := RedisData{
Data: object,
Err: nil,
}
select {
case output <- data:
return true
case <-time.After(5 * time.Second):
err = errors.New("send to output channel timeout")
return false
}
})
if err != nil {
return fmt.Errorf("parse rdb failed: %w", err)
}
return nil
}步驟 2:解析鍵值對 ,判斷是否為大 Key
這是最核心的部分 ,提取對應(yīng)的內(nèi)存大小 ,和task預(yù)設(shè)閾值對比 ,判斷是否為大 Key。
func (b *biz) ExecuteSingleTask(ctx context.Context, task *models.Task) error {
// 1. 提取任務(wù)參數(shù)
pwd := task.Dir
jobID := task.JobID
// 任務(wù)指定的大key閾值,由每個task任務(wù)傳遞
size := task.Size
// 2. 路徑處理
path, err := mypath.GetLastDirAndFiles(pwd)
if err != nil {
return err
}
redisName := path.LastDirName
files := path.Files
slog.Info("process task", "taskID", task.ID, "redisName", redisName, "filesCount", len(files))
// 3. Redis 數(shù)據(jù)處理
ch := make(chan helper.RedisData, 1000)
var wg sync.WaitGroup
// 3.1 啟動生產(chǎn)者協(xié)程(讀取文件并發(fā)送到 channel)
for _, file := range files {
currentFile := pwd + "/" + file
wg.Add(1)
go func(filePath string) {
defer wg.Done()
if err := helper.MyFindBiggestKeys(filePath, ch); err != nil {
slog.Error("producer process file failed", "file", filePath,
"err", err.Error())
}
}(currentFile)
}
// 3.2 啟動協(xié)程:等待生產(chǎn)者完成后關(guān)閉 channel
go func() {
wg.Wait()
close(ch)
slog.Info("task producer done, channel closed", "taskID", task.ID)
}()
// 3.3 消費 channel 數(shù)據(jù)并存儲結(jié)果(
for data := range ch {
if data.Err != nil {
slog.Error("data error", "error", data.Err)
continue
}
if uint64(data.Data.GetSize()) <= *size {
continue
}...
步驟 3: 輸出大 Key 結(jié)果。
解析完成后 ,需要將大 Key 結(jié)果以易讀的格式輸出。我這里實現(xiàn)了數(shù)據(jù)入庫(方便后續(xù)分析或接入運維平臺)。
// 構(gòu)造結(jié)構(gòu)體
rediskey := &models.RedisKey{
JobID: jobID,
RedisName: redisName,
Key: data.Data.GetKey(),
Type: data.Data.GetType(),
Size: int64(data.Data.GetSize()),
CreatedAt: time.Now(),
}
if err := b.ResultV1().CreateTaskResult(ctx, rediskey); err != nil {
slog.Error("operation failed",
"err", err,
"key", data.Data.GetKey(),
)
}
slog.Info("received data",
"key", data.Data.GetKey(),
"type", data.Data.GetType(),
"size", data.Data.GetSize(),
)
}
return nil
}3. 性能優(yōu)化:讓解析更快
Golang 本身性能已經(jīng)很好 ,但面對超大 RDB 文件( 比如 20GB 以上),還是需要做—些優(yōu)化,主要從以下兩點入手:
優(yōu)化 1 :并發(fā)解析(利用 Golang 的協(xié)程)
RDB 文件是按Redis分片集群來備份導(dǎo)出的 ,核心思路是將不同RDB的解析任務(wù)分配到不同協(xié)程中 ,提升并行處理效率。具體代碼這里就不展開了 ,需要注意協(xié)程安全 ,用 sync.WaitGroup 等待所有協(xié)程完成。
優(yōu)化 2:減少內(nèi)存占用
解析大 RDB 文件時 ,容易出現(xiàn)內(nèi)存暴漲( 比如解析 20GB 的 RDB ,可能需要占用幾十 GB 內(nèi)存)。可以通過 “邊解析邊輸出” 的方式優(yōu)化:解析出—個大 Key 后 ,立即寫入channel ,而不是先存在切片中(避免大量數(shù)據(jù)堆積在內(nèi)存),入庫的協(xié)程讀取channel進行插入數(shù)據(jù)庫。
修改思路:將key ,parse過程中判斷是大 Key 后 ,直接寫入channel文件 ,無需存儲到切片。
三、實際落地:從代碼到運維工具
代碼寫完后 ,還需要做—些 “工程化” 處理 ,讓它成為真正能用的運維工具:
1. 編譯成二進制文件
Golang 可以跨平臺編譯 ,通過 Makefile可以快速支持編譯各種平臺的二進制文件 ,并且快速啟動調(diào)試:
# 運行程序(用于開發(fā)調(diào)試)
run:
@echo "運行程序 ..."
go run $(MAIN_FILE) -c configs/rdb-server.yaml
# 交叉編譯:生成Linux-amd64架構(gòu)的可執(zhí)行文件
build-linux:
@echo "編譯Linux-amd64架構(gòu)程序 ..."
mkdir -p $(OUTPUT_DIR)
GOOS=linux GOARCH=amd64 go build $(GO_BUILD_FLAGS) -o
$(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_FILE)
@echo "Linux版本編譯完成:$(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64"
# 交叉編譯:生成Windows-amd64架構(gòu)的可執(zhí)行文件
build-windows:
@echo "編譯Windows-amd64架構(gòu)程序 ..."
mkdir -p $(OUTPUT_DIR)
GOOS=windows GOARCH=amd64 go build
$(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_FILE)
@echo "Windows版本編譯完成:$(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe"將二進制文件放到服務(wù)器上 ,直接運行即可:
./rdb-bigkey-linux-amd64 -c configs/rdb-server.yaml2. 定時任務(wù)( 自動執(zhí)行)
通過定時任務(wù) ,設(shè)置每天低峰期自動執(zhí)行按集群列表執(zhí)行 RDB 備份任務(wù) ,備份任務(wù)完成后將結(jié)果入task表 ,任務(wù)自動觸發(fā)大key解析處理。
3. 集成到運維平臺
這里沒有開發(fā)前端 ,而是基于Grafana ,將大 Key 從數(shù)據(jù)庫(如 MySQL)讀取 ,再通過Grafana 配置面板 ,展? “每日大 Key 數(shù)量趨勢”“各業(yè)務(wù)線大 Key 分布” 等圖表 ,實現(xiàn)可視化監(jiān)控。公司如果有其他運維平臺 ,也可以集成進去 ,定制更靈活。
四、踩過的坑與解決方案
在實際測試和使用中 ,我遇到了幾個問題 ,這里分享給大家 ,避免踩坑:
坑 1 :RDB 文件格式不兼容
問題:解析某些老版本 Redis(如 Redis 4.0) 的 RDB 文件時 ,出現(xiàn)錯誤。
原因:采用的庫默認支持 Redis 6.0+ ,對老版本個別編碼不兼容。
解決方案:更換為支持多版本的庫 ,或者對解析程序源碼進行二次開發(fā)。
坑 2:解析超大 RDB 文件時內(nèi)存溢出
問題:解析 20GB 的 RDB 文件時 ,程序內(nèi)存占用超過 40GB ,被系統(tǒng) kill。
原因:默認情況下 ,解析器會將整個 RDB 文件的鍵值對加載到內(nèi)存 ,導(dǎo)致內(nèi)存暴漲。
解決方案:啟用 “流式解析” ,邊解析邊釋放內(nèi)存 —— 在 Decode 函數(shù)中 ,每解析完—個數(shù)據(jù)庫的鍵值對 ,就立即處理并釋放該數(shù)據(jù)庫的數(shù)據(jù) ,避免堆積。
五、總結(jié)與后續(xù)計劃
這套 Golang 實現(xiàn)的 RDB 大 Key 解析工具 , 目前已經(jīng)在我們公司的 Redis 集群中穩(wěn)定運行了 2個月 ,相比之前的 Python 工具 ,解析速度提升了 3-5 倍(解析 20GB RDB 文件從 1 小時縮短到15 分鐘左右),而且支持自定義閾值和結(jié)果輸出格式 ,非常靈活。

后續(xù)我計劃在現(xiàn)有基礎(chǔ)上增加兩個功能:
- 業(yè)務(wù)線自動分類:根據(jù)鍵名前綴(如 user:info: 屬于用戶業(yè)務(wù) , order:detail: 屬于訂單業(yè)務(wù)), 自動給大 Key 打上業(yè)務(wù)標簽 ,方便定位責(zé)任團隊;
- 大 Key 增長趨勢分析:將每天的大 Key 結(jié)果存入數(shù)據(jù)庫 ,對比分析 “某鍵是否連續(xù) 3 天為大Key”“內(nèi)存是否持續(xù)增長” ,提前預(yù)警潛在風(fēng)險。
如果你也在做 Redis 大 Key 治理 ,希望這篇實戰(zhàn)分享能給你帶來幫助。如果有更好的優(yōu)化思路或問題 ,歡迎交流!
作者介紹
劉宇,翼支付云原生存儲領(lǐng)域資深專家,深耕有狀態(tài)服務(wù)云原生化全鏈路實踐,聚焦分布式數(shù)據(jù)庫核心技術(shù)攻堅與開發(fā)運維一體化體系的構(gòu)建。






























