一条命令,压垮整个大促
大促前夜的演练,监控上 QPS 曲线毫无征兆地塌了下去。排查半天,真凶让所有人哭笑不得:有同事想统计订单 key 的数量,在生产上敲了一句 keys *。一条命令,全店打烊。这件事之后我们补了两样东西:一份生产禁令,和这篇要讲的性能大局观。
大局观:单线程是铠甲也是软肋
Redis 快,一半功劳是单线程:没有锁竞争、没有上下文切换,纯内存操作一路狂奔。但单线程也是它的软肋——所有请求排一条队,任何一条慢命令都会阻塞后面所有人。keys 的复杂度是 O(N),N 是全库 key 数量:千万级 key 遍历完之前,整条队列瘫痪。所以 Redis 的性能问题,十有八九能归到三类:命令复杂度高(O(N) 命令)、网络往返多(该批量没批量)、单个 key 太大(第 3 篇讲过的大 key 治理)。对症下药即可。
毒药清单与安全替代
| 毒药命令 | 复杂度 | 安全替代 |
|---|---|---|
| keys | O(N),全库遍历 | scan 游标分批遍历 |
| hgetall / smembers | O(N),随 field 数线性 | hscan / sscan,或 hmget 取指定 field |
| del 大 key | O(N),同步释放阻塞 | unlink 异步删除(第 9 篇) |
scan 是 keys 的安全替身:把全库遍历拆成基于游标的分批操作,每批只扫几百个槽,扫完把游标还给你,下一轮接着扫,主线程不会被长时间霸占:
# 游标 0 开始,每批约 500 个,返回新游标 + 命中的 key
scan 0 match order:* count 500Java 里对应的游标遍历写法:
// 分批扫描,不阻塞主线程;用完记得关 Cursor
try (Cursor<String> cursor = redisTemplate.scan(
ScanOptions.scanOptions().match("order:*").count(500).build())) {
while (cursor.hasNext()) {
// 逐批处理
String key = cursor.next();
}
}但 scan 不是 keys 的完美平替,两个语义差异要记住:它可能返回重复 key(业务处理要幂等);遍历期间新增或删除的 key,不保证完整覆盖——它是游标遍历,不是某一时刻的快照。要精确计数,用别的方式。
Pipeline:往返成本才是大头
有人做过账:同机房一次网络往返约 0.5 毫秒,Redis 处理一条命令不到 0.1 毫秒——时间都花在路上了。批量写一万个 key,一万次往返要 5 秒起步。Pipeline 的思路简单粗暴:把一万条命令攒起来一次发送、一次收结果,往返从一万次压成一次:
// 一万次 set:从一万次往返压成一次
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection conn = (StringRedisConnection) connection;
for (int i = 0; i < 10000; i++) {
conn.set("batch:key:" + i, "v" + i);
}
return null;
});两个要点:一是 Pipeline 不保证原子性——它只管攒着一起发,中间可能插进别人的命令,要原子性请用事务或 Lua;二是一次别攒太多,几万条命令堆在缓冲区里,客户端和服务端内存都会吃紧,工程上按几千到一万分批。
慢查询日志:让真凶现形
优化之前得先定位。Redis 自带慢查询日志:执行耗时超过阈值的命令会被记下来(注意是命令执行耗时,不含排队和网络时间,所以慢日志没记录不代表链路没延迟):
# 超过 10 毫秒记一笔,最多保留 500 条
slowlog-log-slower-than 10000
slowlog-max-len 500
# 看最近 5 条慢查询
slowlog get 5
# 1) 1) (integer) 27 # 日志序号
# 2) (integer) 1726100000 # 发生时间戳
# 3) (integer) 15420 # 耗时:15420 微秒 ≈ 15 毫秒
# 4) 1) "keys" # 命令原文
# 2) "*"日志是个先进先出的定长队列,线上建议阈值 10 毫秒、长度留大一点,配合定期导出巡检。配合 info commandstats 还能看到每个命令的调用次数和总耗时占比,毒药命令一目了然。
性能优化清单
- 生产禁令写进评审:keys、全量 hgetall、smembers 上黑名单,code review 时看到就打回。
- 能批量就批量:同类写操作用 Pipeline,读用 mget(注意 Cluster 下同槽要求,见第 13 篇)。
- 大 key 提前拆:设计期就按第 3 篇的口径拆结构,别等慢日志来抓。
- 慢查询日志常开:阈值 10 毫秒,巡检定期看,别等 QPS 塌了才想起它。
总结:单线程模型决定了「一条慢命令等于全站慢」,防毒药靠 scan 替代,省耗时靠 Pipeline,抓真凶靠慢查询日志。但还有一类性能问题藏得更深——所有命令都是快的,可 QPS 全部涌向了同一个 key。下一篇聊聊热 key:一个明星词条怎么打爆单块网卡?探测、打散、本地缓存三件套怎么组合?下篇见。
咖啡凉了,记得趁热喝。
评论 (0)