Redis 从 0 到 1(二十二):一个 500MB 的 Hash 拖垮实例——bigkey 的 Go 渐进删除
我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Redis 从 0 到 1二十二一个 500MB 的 Hash 拖垮实例——bigkey 的 Go 渐进删除一、现象一条 DEL实例阻塞 3 秒二、bigkey 的 4 类成因收藏点 1三、找出 bigkey收藏点 2四、Go 实操渐进删除收藏点 3五、治理 Checklist收藏点 4六、排查步骤怀疑有 bigkey七、总结Redis 从 0 到 1二十二一个 500MB 的 Hash 拖垮实例——bigkey 的 Go 渐进删除上一篇《Redis 从 0 到 1二十一P99 涨到 80ms慢查询日志 5 步定位法 Go 采集器》我们学会了抓慢命令链接https://blog.csdn.net/2501_92769340/article/details/167399878 。本篇处理慢命令最常见的元凶bigkey——怎么找、怎么安全删、怎么预防。一、现象一条 DEL实例阻塞 3 秒运维同学清理一批历史数据脚本里对几百个 key 执行 DEL。其中一个是 500MB 的 Hash某活动的用户报名表。DEL 是同步释放内存Redis 主线程遍历整个 Hash 结构逐个释放——3 秒里实例不响应任何请求上游全部超时。更糟的是这样的 key 往往不止一个。bigkey 的危害不止 DEL操作bigkey 下的表现HGETALL / SMEMBERS全量返回网络 序列化 客户端内存三杀DEL / HDEL 大批量同步释放阻塞主线程主从复制全量同步时大 key 拖慢传输backlog 被冲掉第 8 篇来源 2AOF 重写 / RDB处理大 value 耗时翻倍二、bigkey 的 4 类成因收藏点 1成因典型现场预防无上限集合消息列表、日志队列只增不删LTrim / 定期归档 / Stream MAXLEN第 17、18 篇单个 value 过大把整个 JSON 数组100 万条订单明细塞一个 String拆 key 分页存list:1001:page:1基数失控的 HashHash 存全部商品 id → 信息商品涨到百万级按前缀拆 Hash分桶批量导入未分批一次性导入 500 万 key 的单 value 很大分批 每批验大小三、找出 bigkey收藏点 2# 方法 1采样统计生产常用低侵入redis-cli--bigkeys# 每种类型最大的 key采样估算redis-cli--memkeys# 直接按内存排序Redis 6.2更准确# 方法 2精确测量单个 keyMEMORY USAGE key:xxx[SAMPLES5]# 含内部结构开销的真实字节数MEMORY USAGE key:xxx SAMPLES50# 大 Hash/Set 增加采样数更准OBJECT ENCODING key:xxx# 看编码hashtable 通常意味着大# 方法 3离线全量分析最准从库或 RDB 上做rdb-cmemory dump.rdbmem.csv# 工具github.com/sripathikrishnan/redis-rdb-tools判多大算 bigkey经验阈值——String 100KB、集合类 5000 个元素或 10MB就该警惕结合实例内存看8GB 实例和 256GB 实例标准不同。四、Go 实操渐进删除收藏点 3大 String 用 UNLINK异步释放立即返回大集合类用 SCAN 家族分批删packagemainimport(contextfmttimegithub.com/redis/go-redis/v9redis-0to1/redisx)// DeleteBigHash 渐进删除大 HashHSCAN 分批 HDEL每批之间主动让出主线程funcDeleteBigHash(ctx context.Context,rdb*redis.Client,keystring,batchint64)error{varcursoruint64for{// 1. 扫一批 fieldfields,next,err:rdb.HScan(ctx,key,cursor,*,batch).Result()iferr!nil{returnfmt.Errorf(HSCAN 失败: %w,err)}// 2. 分批删这批 fieldPipeline 打包iflen(fields)0{pipe:rdb.Pipeline()fori:0;ilen(fields);i2{// HScan 返回 field,value 交替pipe.HDel(ctx,key,fields[i])}if_,err:pipe.Exec(ctx);err!nil{returnfmt.Errorf(HDel 批次失败: %w,err)}}cursornextifcursor0{break}// 3. 主动让出主线程每批删完歇 10ms实例继续服务time.Sleep(10*time.Millisecond)}returnnil}// DeleteBigSet / DeleteBigZSet 同理SSCANSREM / ZSCANZREM模板相同funcmain(){rdb,err:redisx.NewClient(redisx.Config{Addr:127.0.0.1:6379,Password:redis123,PoolSize:10})iferr!nil{panic(err)}ctx:context.Background()key:activity:signup// 先量一下有多大usage,_:rdb.MemoryUsage(ctx,key,0).Result()fmt.Printf(%s 占用 %d 字节\n,key,usage)// String 类型的大 keyUNLINK 一步搞定后台线程异步释放t,_:rdb.Type(ctx,key).Result()iftstring{rdb.Unlink(ctx,key)}elseifthash{DeleteBigHash(ctx,rdb,key,500)}fmt.Println(删除完成实例无感)}要点UNLINK 优先Redis 4 的 UNLINK 把释放动作交给后台线程主线程只做摘除大 String 场景一步到位SCAN 家族分批HSCAN/SSCAN/ZSCAN 每批只碰一小部分内存配 HDEL 精确删批间 sleep 让出删 500 个 field 约 1mssleep 10ms 是让单线程喘口气服务别的请求删完验证EXISTSMEMORY USAGE并回看 slowlog 确认无新慢命令。五、治理 Checklist收藏点 4上线前redis-cli --bigkeys进巡检脚本每周跑应用侧所有可能无限增长的集合都设上限LTrim / MAXLEN / 定期归档删除侧禁止 DEL 大 keyString 用 UNLINK、集合用渐进删配置侧lazyfree-lazy-user-del yes——把 DEL 全局改成 UNLINK 语义防手滑监控侧slowlog 采集器第 21 篇 MEMORY USAGE 抽查双保险六、排查步骤怀疑有 bigkeyslowlog 找耗时异常的全量读命令HGETALL/SMEMBERS拿到 keyMEMORY USAGE key确认大小OBJECT ENCODING看编码OBJECT REFCOUNT 从库DEBUG OBJECT看 serializedlength交叉验证按第三节模板渐进删除删完复核 slowlog。七、总结bigkey 的罪不在大在对它做的操作都是 O(N) 且单线程执行找–bigkeys 采样 MEMORY USAGE 精确删UNLINKString/ SCAN 家族分批删集合类批间让出主线程防集合设上限、DEL 改 UNLINK 语义、巡检常态化。下一篇预告bigkey 是大的问题下一篇是热的问题——下一篇《一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现》单槽热点的解法是二级缓存。