☰
Redis 从 0 到 1(二十二):一个 500MB 的 Hash 拖垮实例——bigkey 的 Go 渐进删除
2026/11/5 5:21:18 网站建设 项目流程

我不会起名字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 篇来源 2)
AOF 重写 / 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.rdb>mem.csv# 工具:github.com/sripathikrishnan/redis-rdb-tools

判多大算 bigkey:经验阈值——String > 100KB、集合类 > 5000 个元素或 > 10MB,就该警惕(结合实例内存看,8GB 实例和 256GB 实例标准不同)。

四、Go 实操:渐进删除(收藏点 3)

大 String 用 UNLINK(异步释放,立即返回);大集合类用 SCAN 家族分批删:

packagemainimport("context""fmt""time""github.com/redis/go-redis/v9""redis-0to1/redisx")// DeleteBigHash 渐进删除大 Hash:HSCAN 分批 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. 分批删这批 field(Pipeline 打包)iflen(fields)>0{pipe:=rdb.Pipeline()fori:=0;i<len(fields);i+=2{// HScan 返回 field,value 交替pipe.HDel(ctx,key,fields[i])}if_,err:=pipe.Exec(ctx);err!=nil{returnfmt.Errorf("HDel 批次失败: %w",err)}}cursor=nextifcursor==0{break}// 3. 主动让出主线程:每批删完歇 10ms,实例继续服务time.Sleep(10*time.Millisecond)}returnnil}// DeleteBigSet / DeleteBigZSet 同理:SSCAN+SREM / ZSCAN+ZREM,模板相同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 类型的大 key:UNLINK 一步搞定(后台线程异步释放)t,_:=rdb.Type(ctx,key).Result()ift=="string"{rdb.Unlink(ctx,key)}elseift=="hash"{DeleteBigHash(ctx,rdb,key,500)}fmt.Println("删除完成,实例无感")}

要点:

  1. UNLINK 优先:Redis 4+ 的 UNLINK 把释放动作交给后台线程,主线程只做摘除,大 String 场景一步到位;
  2. SCAN 家族分批:HSCAN/SSCAN/ZSCAN 每批只碰一小部分内存,配 HDEL 精确删;
  3. 批间 sleep 让出:删 500 个 field 约 1ms,sleep 10ms 是让单线程喘口气服务别的请求;
  4. 删完验证:EXISTS+MEMORY USAGE,并回看 slowlog 确认无新慢命令。

五、治理 Checklist(收藏点 4)

  • 上线前:redis-cli --bigkeys进巡检脚本,每周跑
  • 应用侧:所有"可能无限增长"的集合都设上限(LTrim / MAXLEN / 定期归档)
  • 删除侧:禁止 DEL 大 key,String 用 UNLINK、集合用渐进删
  • 配置侧:lazyfree-lazy-user-del yes——把 DEL 全局改成 UNLINK 语义,防手滑
  • 监控侧:slowlog 采集器(第 21 篇)+ MEMORY USAGE 抽查,双保险

六、排查步骤:怀疑有 bigkey

  1. slowlog 找耗时异常的全量读命令(HGETALL/SMEMBERS),拿到 key;
  2. MEMORY USAGE key确认大小,OBJECT ENCODING看编码;
  3. OBJECT REFCOUNT+ 从库DEBUG OBJECT(看 serializedlength)交叉验证;
  4. 按第三节模板渐进删除,删完复核 slowlog。

七、总结

  1. bigkey 的罪不在"大",在"对它做的操作都是 O(N) 且单线程执行";
  2. 找:–bigkeys 采样 + MEMORY USAGE 精确;
  3. 删:UNLINK(String)/ SCAN 家族分批删(集合类),批间让出主线程;
  4. 防:集合设上限、DEL 改 UNLINK 语义、巡检常态化。

下一篇预告:bigkey 是"大"的问题,下一篇是"热"的问题——下一篇《一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现》,单槽热点的解法是二级缓存。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询