Redis 核心命令单线程串行。一条 O(N) 慢命令就能把节点 CPU 打满,扩容也接不住。破局三层:入口管控、慢查询与 SCAN 拆分、集群分片。
背熟三条原因,栽在一条 SMEMBERS 上
候选人四年后端。问 Redis 单线程为什么还这么快,脱口而出:纯内存操作比磁盘快几个数量级,单线程省掉上下文切换和锁竞争开销,再配上 IO 多路复用处理海量连接,天然高性能。
三句话没错,只能算背熟了。
我追问的是线上真实事故:核心商品缓存集群日常跑几万 QPS,延迟稳定在毫秒级,一直没出过性能问题。某次运营批量导入商品标签后,商品分类接口大面积超时,Redis 单节点 CPU 直接打到 100%,整条商品查询链路跟着卡顿。
排查下来,就是一条 SMEMBERS 查了一个百万级大集合,单次执行 200 多毫秒。就这一条慢命令,把整个节点上的所有请求都卡住了。
更气人的是,这个集群本来有多个节点分摊流量。但因为某一个节点的单线程被阻塞,落到这个节点的请求全部超时。扩容新节点也没用------只要那条命令还在跑,这个节点就一直处于阻塞状态。
候选人想了想说,大 Key 导致的慢查询。我再追:大 Key 拆分我知道,但既然单线程这么高效,为什么一条命令就能拖垮整个实例?多线程并行处理难道不是更快?Redis 6.0 为什么又要引入多线程?
答不上来,面试结束。
200 毫秒,等于几万条正常命令的执行窗口
要理解这个事,先得把"单线程"三个字说准确:Redis 的单线程,指的是核心命令执行是单线程,不是整个进程只有一个线程。
持久化刷盘、过期 Key 清理、主从同步都有独立的后台线程在干活。但客户端发来的读写命令,是在同一个主线程里排队串行执行的------没有第二个线程能帮你分担其中一条。
于是阻塞链路是这样的:
关键在于 SMEMBERS 的复杂度是 O(N),N 是一百万。200 毫秒里主线程跑不动任何别的命令,而正常命令的处理时间是微秒级。200 毫秒 ≈ 正常情况下几万条命令的执行窗口,这段时间全被浪费在一条命令上。
这时候谈扩容是无效的。扩容解决的是"流量分摊",解决不了"单条命令的耗时"。大 Key 只落在某一个分片上,压力就全压在那一台。
Redis 6.0 多线程,加的不是命令执行线程
这是最容易被误解的一层。Redis 6.0 引入多线程,多出来的是 IO 线程,不是命令执行线程。
网络包的收发、协议解析被分摊给多个 IO 线程,主线程的算力被释放出来专注做命令执行。数据结构的读写仍然保持单线程,为的是不引入锁、不破坏数据结构的线程安全性。
所以 6.0 的多线程治的是"网络 IO 成为瓶颈"的高并发场景,治不了"单条命令自己就很慢"。 你把 IO 线程开到 8 个,SMEMBERS 还是得主线程跑完那 200 毫秒。
面试官想听的不是"Redis 为什么快"
面试官真正想听的,是你有没有从"背书"升级成"全场景性能治理"的工程思维。
入口级管控:O(N) 命令别出现在核心链路
单线程的性能优势有明确适用范围,不是所有场景都通用。生产环境要对 O(N) 类操作做入口级管控:
| 高危命令 | 复杂度 | 生产替代方案 |
|---|---|---|
KEYS * |
O(N) | SCAN 游标分批 |
SMEMBERS 大集合 |
O(N) | SSCAN 迭代 |
HGETALL 大 Hash |
O(N) | HSCAN 迭代 |
FLUSHALL |
O(N) | 按前缀分批清理 |
LRANGE key 0 -1 |
O(N) | 分页读取 |
常规业务命令优先选 O(1) 或 O(logN) 的写法,别让无节制的全量操作出现在核心链路里。
慢查询日志只是底线,命令还得拆
光靠人记命令清单不够,得有体系。
线上开启慢查询日志,把阈值压到能感知的程度:
bash
# 记录超过 10ms 的命令,慢查询日志保留 128 条
CONFIG SET slowlog-log-slower-than 10000
CONFIG SET slowlog-max-len 128
# 看最近的慢命令
SLOWLOG GET 10
同时实时采集命令执行时长,超阈值自动告警,并且能定位到异常的大请求来源,避免慢命令持续占用主线程。
慢命令本身要拆。用游标迭代替代全量返回,把一次长耗时的操作拆成多次短耗时的操作:
java
String cursor = "0";
ScanParams params = new ScanParams().count(500);
do {
ScanResult<String> result = jedis.sscan("product:tag:all", cursor, params);
process(result.getResult()); // 处理这一批
cursor = result.getCursor();
} while (!"0".equals(cursor)); // 游标归零才算遍历完
百万级集合被切成 2000 次亚毫秒级的调用,主线程随时能腾出手处理其他请求。注意 SSCAN 只保证弱一致性,迭代过程中集合发生变更,可能返回重复元素,业务侧要能容忍或者做去重。
把单线程的风险转移到架构层
命令层治完,还得从根上把单线程的风险转移出去。
- 热点大 Key 分片拆分 :把大 Set、大 Hash 按业务维度拆成多个小 Key,分散到不同节点,单节点的单线程压力被摊到整个集群。比如
product:tag:all拆成product:tag:{categoryId}。 - 复杂计算、批量统计统一走从节点:不占用主节点的主线程资源,非核心操作不许影响核心业务链路。
- 大促前做专项慢查询巡检:提前清理历史大 Key,压测复杂命令的实际耗时,把高危操作入口禁掉。
宁可牺牲一部分非核心操作的便利性,也绝不允许单条慢命令拖垮整个缓存节点。
单线程不是免死金牌
Redis 单线程模型的性能优势,只针对内存级的简单操作。看不清命令执行的复杂度边界,慢查询的阻塞风险就会从一条命令扩散成整条链路雪崩。
普通开发以为单线程就是 Redis 高性能的全部答案;高级工程师知道,在复杂的数据规模和并发时长面前,单线程模型是一把双刃剑。
写在最后
面试里这道题真正的考点,是有没有从"背熟单线程快的三个原因"升级为"设计全场景的性能治理体系"。
你们线上是怎么管 Redis 慢命令的------是拆 Key、用 proxy 在入口层拦截高危命令,还是直接靠监控告警事后 kill?评论区聊聊你们的做法。
有用的话点个赞。