资讯详情

Redis 从 0 到 1(四):一条 KEYS * 拖垮网关 30 秒——key 规范与 SCAN 的 Go 实现

📅 2026/10/11 8:48:10 | 华诺云谱 👁 阅读
Redis 从 0 到 1(四):一条 KEYS * 拖垮网关 30 秒——key 规范与 SCAN 的 Go 实现
我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Redis 从 0 到 1四一条 KEYS * 拖垮网关 30 秒——key 规范与 SCAN 的 Go 实现一、事故现象网关 30 秒集体超时二、为什么 KEYS * 这么危险三、key 命名 4 条规范收藏点 1四、Go 实操SCAN 渐进遍历 批量处理模板收藏点 2五、命令清单收藏点 3六、线上找 key 的正确姿势排查步骤七、总结Redis 从 0 到 1四一条 KEYS * 拖垮网关 30 秒——key 规范与 SCAN 的 Go 实现上一篇《Redis 从 0 到 1三五大结构 20 个最常用命令用 Go 各写一个真实用法》我们把 5 个场景的 20 个命令过了一遍链接https://blog.csdn.net/2501_92769340/article/details/167399371 。本篇讲一个由一条命令引发的真实事故以及 key 的工程化管理。一、事故现象网关 30 秒集体超时某晚 22 点网关突然大面积 504持续约 30 秒后自行恢复。应用日志、数据库、网络全部正常。最后定位到 Redis 慢查询日志里躺着这样一条1) 1) (integer) 7 2) (integer) 1696300000 3) (integer) 30000000 # 耗时 30 秒单位微秒 4) 1) KEYS 2) *原因一位同事想在生产库里看看有哪些 key执行了KEYS *。库里 5000 万个 keyKEYS 是 O(N) 全表扫描而Redis 处理命令是单线程的——这 30 秒里Redis 不处理任何其他命令所有业务请求排队直到超时。二、为什么 KEYS * 这么危险命令复杂度5000 万 key 实测对实例影响KEYS *O(N)约 30 秒阻塞全实例停摆KEYS user:*O(N)同样扫全库再匹配全实例停摆SCAN 0 MATCH user:* COUNT 1000单次 O(1)~O(COUNT)每次约 1ms分批无感KEYS 的问题不是慢是它把单线程堵死了。这和第 5 篇要讲的单线程模型直接相关Redis 快是因为每条命令都很快任何一条 O(N) 大命令都会变成全局红灯。防护第一招直接在服务端把危险命令改名或禁用redis.conf# 禁用改名为空字符串 rename-command KEYS rename-command FLUSHALL rename-command FLUSHDB # 或者改成一个只有运维知道的名字 rename-command CONFIG CONFIG_x9k2三、key 命名 4 条规范收藏点 1事故的另一半原因是 key 太乱同事才不得不用 KEYS 去找。规范先行业务:实体:ID[:字段]四段式order:detail:10086、user:1001:cart冒号分隔、全部小写、见名知意key 长度控制在 44 字节内过长的 key 本身吃内存5000 万个 key 多 20 字节就是 1GB但别为了省内存起a:b:c这种看不懂的名字禁止特殊字符空格、换行、中文都会让 CLI 排查时怀疑人生每个 key 都必须有 TTL 或淘汰理由缓存类必带 TTL持久类如购物车写进文档说明为什么不淘汰防止 key 只增不减把内存吃满。TTL 管理的两个实用命令TTL user:1001# 剩余秒数-1 永不过期要警惕-2 不存在EXPIRE user:10013600# 补设 TTL四、Go 实操SCAN 渐进遍历 批量处理模板收藏点 2需求找出所有session:*里剩余 TTL 不足 10 分钟的 key 并续期——5000 万 key 的库里安全完成。packagemainimport(contextfmttimeredis-0to1/redisx)// ScanEach 渐进遍历匹配 pattern 的 key每批 callback全程不阻塞实例funcScanEach(ctx context.Context,rdb redis.Scripter,patternstring,batchFnfunc(keys[]string)error)error{client:rdb.(*redis.Client)varcursoruint64for{// 每次最多扫 COUNT 个 key 槽位耗时 ~1ms实例无感keys,next,err:client.Scan(ctx,cursor,pattern,1000).Result()iferr!nil{returnfmt.Errorf(SCAN 失败(cursor%d): %w,cursor,err)}iflen(keys)0{iferr:batchFn(keys);err!nil{returnerr}}cursornextifcursor0{// 游标归零 完整扫完一圈returnnil}}}funcmain(){rdb,err:redisx.NewClient(redisx.Config{Addr:127.0.0.1:6379,Password:redis123,PoolSize:20,MinIdleConns:5,})iferr!nil{panic(err)}deferrdb.Close()ctx:context.Background()// 长任务不用短超时但每步操作仍受客户端 ReadTimeout 约束varrenewedinterrScanEach(ctx,rdb,session:*,func(keys[]string)error{// 批量处理用 Pipeline一次 RTT 拿一批 TTL第 24 篇专门讲pipe:rdb.Pipeline()cmds:make([]*redis.DurationCmd,len(keys))fori,k:rangekeys{cmds[i]pipe.TTL(ctx,k)}if_,err:pipe.Exec(ctx);err!nil{returnfmt.Errorf(Pipeline TTL 失败: %w,err)}fori,ttl:rangecmds{remain,err:ttl.Result()iferrnilremain0remain10*time.Minute{iferr:rdb.Expire(ctx,keys[i],30*time.Minute).Err();err!nil{returnerr}renewed}}returnnil})iferr!nil{panic(err)}fmt.Printf(扫描完成续期了 %d 个 session key\n,renewed)}SCAN 的三个使用要点COUNT 只是提示不是精确每批数量给 1000 是经验值调太大如 100000单批也会阻塞SCAN 不保证不重不漏遍历期间 key 空间在变化可能重复返回代码里续期操作幂等重复无害要求精确快照就先BGSAVE拿 RDB 离线分析家族成员Hash 大 key 用HSCAN、Set 用SSCAN、ZSet 用ZSCAN语义相同——第 22 篇删 bigkey 全靠它们。五、命令清单收藏点 3命令作用注意SCAN cursor [MATCH p] [COUNT n]渐进遍历 key 空间游标归零才算完HSCAN/SSCAN/ZSCAN渐进遍历大 key 内部删 bigkey 必备KEYS pattern全库匹配生产禁用OBJECT ENCODING key看内部编码第 26 篇内存优化用MEMORY USAGE key看 key 占多少字节找 bigkey 用rename-command配置项禁用/改名命令改完要重启生效RANDOMKEY随机看一个 key排查小偏方六、线上找 key 的正确姿势排查步骤先确认库量级DBSIZE百万以下才能考虑快速操作明确要找什么能用精确 key 就不用模糊匹配GET order:detail:10086必须模糊找从库/离线 RDB 上执行或用 SCAN 分批每批 COUNT 1000怀疑有危险命令被执行过查慢查询SLOWLOG GET 10第 21 篇细讲。七、总结KEYS *的危险不在慢在单线程全实例阻塞生产用rename-command直接禁掉key 命名四段式 长度控制 必带 TTL 或淘汰理由遍历一律 SCAN 家族配 Pipeline 批量处理回调逻辑保持幂等找 key 优先精确查询必须模糊找就去从库或离线做。下一篇预告这一篇反复提到Redis 是单线程的一条慢命令堵全局——那问题来了单线程凭什么还能扛 10 万 QPS下一篇《单线程凭什么 10 万 QPSepoll 事件循环拆解 Go 压测验证 3 个结论》我们把事件循环拆开再用 Go 写压测程序实测验证。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑