这个主流大法竟是大坑!Redis 大 Key 问题怎么破?

刘宇 2025-11-12 09:47:08
作者介绍

刘宇,翼支付云原生存储领域资深专家,深耕有状态服务云原生化全链路实践,聚焦分布式数据库核心技术攻坚与开发运维一体化体系的构建。

 

在 Redis 运维中 ,大 Key 始终是威胁集群稳定性的 “ 隐形炸弹”。我梳理 Redis 大 Key 解析系统时 ,在比较了scan查找大key方案 ,以及使用官方的redis-cli --bigkeys方案后 ,选择 “基于 RDB的离线解析” 方案 —— 它完全不影响在线集群 ,还能获取全量键的精确内存数据 ,特别适合非实时性的大 Key 排查场景(实时的可以用慢查询系统 ,这个以前曾在dbaplus分享过)。最近我用 Golang 实现了这套方案 ,今天就从场景需求、技术实现到实际落地 ,和大家详细分享整个过程。

 

一、为什么要造这个 “轮子”?

 

市面上其实有现成的 RDB 解析工具( 比如 Python 写的 redis-rdb-tools ),但在实际运维中,我遇到了两个关键问题:

 

  • 性能瓶颈:面对 GB 级甚至更大的 RDB 文件 ,Python 工具解析耗时过长( 曾试过解析20GB 的 RDB ,跑了近 1 小时),而运维场景中常需要批量处理多集群的 RDB 文件 ,效率亟待提升;

     

  • 定制化不足:现有工具输出的结果多是通用格式 ,无法直接对接我们内部的运维平台( 比如按业务线分类大 Key、 自动同步结果到 Grafana 面板),需要额外写脚本二次处理。

 

考虑到 Golang 的并发优势和高性能特性 ,以及能直接编译成二进制文件(方便在不同服务器部署),我决定用 Golang 从零实现—套 RDB 离线解析工具 ,核心目标是:快解析、可定制、易集成。

 

二、Golang 实现 RDB 解析的核心思路

 

要解析 RDB 文件 ,首先得理解其二进制格式 ——Redis 的 RDB 文件是按特定协议存储的二进制数据 ,包含数据库选择、键值对数据、过期时间等信息。整个实现过程可以拆成 3 个核心步骤:

 

 

1. 准备工作:选择合适的 RDB 解析库

 

自己手写 RDB 格式解析会耗费大量时间(要处理各种数据类型、压缩格式),Golang 生态中有成熟的 RDB 解析库可以复用, 选择了  https://github.com/HDT3213/rdb (轻量、文档清晰,支持 Redis 6.0 + 的 RDB 格式),本来打算导入这个库的解析能力扩展 ,但研究发现这个库不支持外部导出为内部属性 ,不能完全满足需求 ,考虑再三 ,最后决定通过对项目进行部分魔改 ,通过replace依赖模块指向魔改版本来处理。

 

 

2. 核心流程:从 RDB 文件到大 Key 数据

 

整个解析大key的流程很清晰:备份 RDB 文件 → 读取 RDB 文件 → 解析键值对与内存信息 →筛选大 Key → 输出结果 ,下面—步步拆解关键代码。

 

步骤 1 :读取 RDB 文件并初始化解析器

 

首先要打开 RDB 文件 ,然后用 NewDecoder 初始化解析器, 同时定义—个 “结果处理器”(用来接收解析出的每—个键值对数据)。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 通过一个channel返回解析好的数据,因为有多个rdb要解析,这里传入一个channel统一接收,方便管理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

 

这是最核心的部分 ,提取对应的内存大小 ,和task预设阈值对比 ,判断是否为大 Key。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func (b *biz) ExecuteSingleTask(ctx context.Context, task *models.Task) error {    // 1. 提取任务参数    pwd := task.Dir    jobID := task.JobID    // 任务指定的大key阈值,由每个task任务传递    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 数据处理    ch := make(chan helper.RedisData, 1000)    var wg sync.WaitGroup
    // 3.1 启动生产者协程(读取文件并发送到 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 启动协程:等待生产者完成后关闭 channel        go func() {            wg.Wait()            close(ch)            slog.Info("task producer done, channel closed""taskID", task.ID)         }()
        // 3.3 消费 channel 数据并存储结果(        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 结果。

 

解析完成后 ,需要将大 Key 结果以易读的格式输出。我这里实现了数据入库(方便后续分析或接入运维平台)。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
        // 构造结构体        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. 性能优化:让解析更快

 

Golang 本身性能已经很好 ,但面对超大 RDB 文件( 比如 20GB 以上),还是需要做—些优化,主要从以下两点入手:

 

优化 1 :并发解析(利用 Golang 的协程)

 

RDB 文件是按Redis分片集群来备份导出的 ,核心思路是将不同RDB的解析任务分配到不同协程中 ,提升并行处理效率。具体代码这里就不展开了 ,需要注意协程安全 ,用 sync.WaitGroup 等待所有协程完成。

 

优化 2:减少内存占用

 

解析大 RDB 文件时 ,容易出现内存暴涨( 比如解析 20GB 的 RDB ,可能需要占用几十 GB 内存)。可以通过 “边解析边输出” 的方式优化:解析出—个大 Key 后 ,立即写入channel ,而不是先存在切片中(避免大量数据堆积在内存),入库的协程读取channel进行插入数据库。

 

修改思路:将key ,parse过程中判断是大 Key 后 ,直接写入channel文件 ,无需存储到切片。

 

三、实际落地:从代码到运维工具

 

代码写完后 ,还需要做—些 “工程化” 处理 ,让它成为真正能用的运维工具:

 

 

1. 编译成二进制文件

 

Golang 可以跨平台编译 ,通过 Makefile可以快速支持编译各种平台的二进制文件 ,并且快速启动调试:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    # 运行程序(用于开发调试)    run:        @echo "运行程序 ..."        go run $(MAIN_FILE) -c configs/rdb-server.yaml
    # 交叉编译:生成Linux-amd64架构的可执行文件    build-linux:        @echo "编译Linux-amd64架构程序 ..."        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架构的可执行文件    build-windows:        @echo "编译Windows-amd64架构程序 ..."        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"

 

将二进制文件放到服务器上 ,直接运行即可:

 

  •  
   ./rdb-bigkey-linux-amd64  -c configs/rdb-server.yaml

 

 

2. 定时任务( 自动执行)

 

通过定时任务 ,设置每天低峰期自动执行按集群列表执行 RDB 备份任务 ,备份任务完成后将结果入task表 ,任务自动触发大key解析处理。

 

 

3. 集成到运维平台

 

这里没有开发前端 ,而是基于Grafana ,将大 Key 从数据库(如 MySQL)读取 ,再通过Grafana 配置面板 ,展⽰ “每日大 Key 数量趋势”“各业务线大 Key 分布” 等图表 ,实现可视化监控。公司如果有其他运维平台 ,也可以集成进去 ,定制更灵活。

 

四、踩过的坑与解决方案

 

在实际测试和使用中 ,我遇到了几个问题 ,这里分享给大家 ,避免踩坑:

 

 

坑 1 :RDB 文件格式不兼容

 

问题:解析某些老版本 Redis(如 Redis 4.0) 的 RDB 文件时 ,出现错误。

 

原因:采用的库默认支持 Redis 6.0+ ,对老版本个别编码不兼容。

 

解决方案:更换为支持多版本的库 ,或者对解析程序源码进行二次开发。

 

 

坑 2:解析超大 RDB 文件时内存溢出

 

问题:解析 20GB 的 RDB 文件时 ,程序内存占用超过 40GB ,被系统 kill。

 

原因:默认情况下 ,解析器会将整个 RDB 文件的键值对加载到内存 ,导致内存暴涨。

 

解决方案:启用 “流式解析” ,边解析边释放内存 —— 在 Decode 函数中 ,每解析完—个数据库的键值对 ,就立即处理并释放该数据库的数据 ,避免堆积。

 

五、总结与后续计划

 

这套 Golang 实现的 RDB 大 Key 解析工具 , 目前已经在我们公司的 Redis 集群中稳定运行了 2个月 ,相比之前的 Python 工具 ,解析速度提升了 3-5 倍(解析 20GB RDB 文件从 1 小时缩短到15 分钟左右),而且支持自定义阈值和结果输出格式 ,非常灵活。

 

 

后续我计划在现有基础上增加两个功能:

 

  • 业务线自动分类:根据键名前缀(如 user:info: 属于用戶业务 , order:detail: 属于订单业务), 自动给大 Key 打上业务标签 ,方便定位责任团队;

     

  • 大 Key 增长趋势分析:将每天的大 Key 结果存入数据库 ,对比分析 “某键是否连续 3 天为大Key”“内存是否持续增长” ,提前预警潜在风险。

 

如果你也在做 Redis 大 Key 治理 ,希望这篇实战分享能给你带来帮助。如果有更好的优化思路或问题 ,欢迎交流!

 

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告