2100 字
约 7 分钟
15
S12-A24 · 阻塞命令:`KEYS` 之后,还有五个会让 Redis 卡住的命令

一个服务的 Redis 偶发"几百毫秒无响应",监控上看"CPU 不高、连接数正常"。查了两周,最后发现是"某个后台任务在跑 KEYS user:*"——Redis 是单线程执行命令,这个"排队"的性质很关键:Redis 不会"只卡那一个客户端"——于是"一个后台脚本"能让"整个集群"的 P99 变差,而"根因"在另一条连接上。
一条慢命令会让所有请求排队。

Redis 的"卡"几乎都来自"单线程 + 慢命令"。而"慢"有两类:O(n) 的遍历,以及阻塞式的等待。第二类更隐蔽:它"不耗 CPU",所以监控上看不出来。

1. 三类"会让 Redis 卡"的命令

类 例子
O(n) 遍历 KEYS、HGETALL(大 hash)、SMEMBERS(大 set)
阻塞式 BLPOP、WAIT、BRPOPLPUSH
大规模写 DEL 大 key、FLUSHALL

第二类"不耗 CPU 却占着线程":**BLPOP 在没有元素时会"挂住连接"**第二类的"正确理解"是:Redis 用事件驱动"挂起"这些客户端,不占 CPU、不挡别人——真正出问题的是"业务线程":BLPOP 阻塞了调用它的那个线程。
——它不影响别人(因为 Redis 用"事件驱动"处理阻塞命令),但"配错超时"会"挂住业务线程"。

第三项的"量化":DEL 一个 100 万元素的 hash 要"逐个释放",在"内存分配器"下可能几十到几百毫秒——而这段时间里,所有命令排队。
第三项是"单条命令耗时长":DEL 一个百万元素的 key 会阻塞几百毫秒——而它看起来只是"删一个 key"。

2. "O(n)"的三种替代

原命令 替代
KEYS pattern SCAN cursor MATCH pattern COUNT 500
HGETALL(大 hash) HSCAN 分批
SMEMBERS(大 set) SSCAN 分批

SCAN 的语义是"游标遍历":它不保证"一次拿全",但保证"不漏"SCAN 的"保证"有个前提:遍历期间"不增删"——如果在遍历中"不断新增 key",可能"漏掉"也可能"重复";所以"边界"要接受"弱一致"。
(在"遍历期间没有增删"的前提下)——于是"业务上要接受'分页'"。

// 两种"找 key"的方式
KEYS user:*                     → 一次遍历全库,阻塞(禁用)
SCAN 0 MATCH user:* COUNT 500   → 每次 500 个,多次往返(可用)
// 代价:客户端要"循环到游标为 0",且"总耗时更长"(但每次很短)

3. "大 key"的三种操作代价

操作 代价
DEL big 一次释放全部(阻塞)
UNLINK big 后台释放(4.0+,推荐)
HSCAN + 逐个 HDEL 分批删(最慢但最稳)

第二项的"代价"是"内存不会立刻降":UNLINK 之后 used_memory 是"慢慢降"——于是"看着内存没降"会让人以为"没删掉"。
第二项是"标准做法":UNLINK 把"释放内存"放到后台线程——于是"删除"变成"O(1)"。

"Lua 脚本"也算"单条命令",于是"长脚本"与 KEYS 同类:

// 两类"脚本"的差别
短脚本(< 1ms):判断 + 修改,安全(如库存预扣)
长脚本(循环 10 万次):阻塞整个实例,与 KEYS 同级
// 判据:脚本里的循环次数"是否随业务数据量增长"

4. "阻塞命令"的正确用法

命令 注意
BLPOP key 5 超时要设(否则"永久挂")
WAIT 1 100 会"阻塞等待副本确认"
BRPOPLPUSH 同上 + 顺序风险

第一项在"队列"场景常用:超时设 1–5 秒第一项的"设计"要配"退出信号":消费线程"超时返回 → 检查停止标志 → 再阻塞"——于是"停机"不必等"队列空"。
,于是"没有任务时"客户端能"定期回来检查状态"(如"是否该退出")。

"慢在哪"要用"两个日志"分开看:慢命令日志与客户端列表。

// 两条命令(一次排查,30 秒)
redis-cli SLOWLOG GET 10            # 最近的慢命令(含参数摘要)
redis-cli CLIENT LIST | awk '{print $2, $3}' | sort | uniq -c | sort -rn | head
// 判据:SLOWLOG 有"KEYS/HGETALL" → O(n);CLIENT LIST 有大量 blpop → 阻塞/队列

5. "CPU 不高但卡"的三种可能

可能 判据
慢命令(O(n)) SLOWLOG 里有记录
阻塞命令挂着 CLIENT LIST 里 cmd=blpop
网络/带宽打满 吞吐正常但延迟上升

第一项的"默认值"值得记住:slowlog-log-slower-than 默认 10000 微秒(10ms),而"只记'执行时间'、不含'排队时间'"——于是"卡 500ms 但单条都快"时,它帮不上。
第一项是最直接的:SLOWLOG GET 10 会列出"超过阈值的命令 + 耗时"——而"阈值默认 10ms",多数团队从没看过它。

"大 key"要能"提前发现",否则只能"出事再查":

// 两种"扫大 key"的方式(按成本)
redis-cli --bigkeys                 # 采样统计(快,但可能漏)
redis-cli --memkeys                 # 按内存排序(较慢,更准)
// 判据:单个 key > 10MB 或"元素数 > 10 万" → 列入"改造清单"

6. 排错:五种"Redis 卡顿"

现象 成因 处理方向
偶发几百毫秒无响应 KEYS/大 key 操作 改 SCAN/UNLINK
删除后卡一下 DEL 大 key 改 UNLINK
队列消费者"卡住" BLPOP 超时太长 缩短超时
主从延迟导致读旧值 写入量大 + WAIT 关键读走主
吞吐正常但 P99 高 单条慢命令 看 SLOWLOG

"分批"的"批大小"要选,太小"往返多"、太大"单次卡":

// 一条"够用"的批量规则
SCAN COUNT 500:单次 ≈ 1–3ms(500 个 key 的匹配)
SCAN COUNT 5000:单次 ≈ 10–30ms(可能进 SLOWLOG)
// 判据:单次耗时 < 5ms(P99 预算的 1/10),条数对应"可观测的往返成本"

7. 边界:什么情况"O(n)"可以接受

场景 为什么 做法
key 数量极少(< 1000) KEYS 只扫千级 可接受
只在"运维脚本"里跑 无并发请求 可接受(但避开高峰)
数据结构"天然小"(如配置 hash) HGETALL 只有几十字段 可接受
已有"独立实例"承载批任务 与在线隔离 可接受

判断口径一句话:"这条命令的最坏耗时,会不会超过'你的 P99 预算'"。最后一条与"上线流程"有关的边界:"能在生产上跑 O(n) 命令"这件事必须"被禁止在流程里"——比如"禁止在应用代码里出现 KEYS"(可用静态扫描),否则"这次修了、下次又有人写"。
会(KEYS、大 DEL、大 HGETALL),必须改;不会(小结构、低峰脚本),它只是"理论风险"。

补一个"集群"视角:SCAN 在集群模式下"只扫当前节点"——要"扫全库"必须"对每个主节点各扫一遍"。而很多人以为"一条 SCAN 就遍历全局",于是"只找到了 1/3 的 key",还以为"数据丢了"。

还有一条"运行方式"的建议:把"运维脚本"与"在线实例"分开——KEYS 只在"从库/独立实例"上跑。因为"运维方便"与"在线延迟"本来就该两条路,强行共用一套实例,早晚会出现"为了一次排查,全站卡一秒"。

而"分开"的成本,只是一条连接串。

最后一句:"卡"这个问题上,Redis 从不会"自己变慢",它总是"被某条命令卡住"。

8. 自测

  1. 三类"会让 Redis 卡"的命令分别是什么?为什么"阻塞式"在监控上看不出来?
  2. DEL 与 UNLINK 的差别是什么?为什么"删一个 key"会阻塞几百毫秒?
  3. SCAN 的语义是什么?它"不保证"什么?业务上要接受什么代价?
  4. BLPOP 的超时为什么要设?"永久挂"会挂住谁?
  5. "CPU 不高但卡"的三种可能怎么区分?SLOWLOG 的默认阈值是多少?
  6. 判断"O(n) 能否接受"的那个问题是什么?
S12-A24 · 阻塞命令:`KEYS` 之后,还有五个会让 Redis 卡住的命令
https://www.clxhxhhr.top/posts/6658/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。