连载中 14/16

性能优化:keys 是毒药,Pipeline 是解药

2026-05-22 · 5026 阅读 · 0 评论 · 0 赞

一条命令,压垮整个大促

大促前夜的演练,监控上 QPS 曲线毫无征兆地塌了下去。排查半天,真凶让所有人哭笑不得:有同事想统计订单 key 的数量,在生产上敲了一句 keys *。一条命令,全店打烊。这件事之后我们补了两样东西:一份生产禁令,和这篇要讲的性能大局观。

大局观:单线程是铠甲也是软肋

Redis 快,一半功劳是单线程:没有锁竞争、没有上下文切换,纯内存操作一路狂奔。但单线程也是它的软肋——所有请求排一条队,任何一条慢命令都会阻塞后面所有人。keys 的复杂度是 O(N),N 是全库 key 数量:千万级 key 遍历完之前,整条队列瘫痪。所以 Redis 的性能问题,十有八九能归到三类:命令复杂度高(O(N) 命令)、网络往返多(该批量没批量)、单个 key 太大(第 3 篇讲过的大 key 治理)。对症下药即可。

毒药清单与安全替代

毒药命令复杂度安全替代
keysO(N),全库遍历scan 游标分批遍历
hgetall / smembersO(N),随 field 数线性hscan / sscan,或 hmget 取指定 field
del 大 keyO(N),同步释放阻塞unlink 异步删除(第 9 篇)

scan 是 keys 的安全替身:把全库遍历拆成基于游标的分批操作,每批只扫几百个槽,扫完把游标还给你,下一轮接着扫,主线程不会被长时间霸占:

# 游标 0 开始,每批约 500 个,返回新游标 + 命中的 key
scan 0 match order:* count 500

Java 里对应的游标遍历写法:

// 分批扫描,不阻塞主线程;用完记得关 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:一个明星词条怎么打爆单块网卡?探测、打散、本地缓存三件套怎么组合?下篇见。

咖啡凉了,记得趁热喝。

503

10 年全栈工程师 · 503咖啡馆主理人

#Redis#性能优化#scan#pipeline#慢查询

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞