Skip to content

07|性能与延迟

Redis 通常应该在微秒级响应,但有时会突然变慢。先跑一条延迟探测看看当前状况:

bash
redis-cli --latency -h 127.0.0.1 -p 6379
# min: 0, max: 15, avg: 0.45 (7088 samples)

avg 在 1ms 以内是正常的。如果 avg 跳到几毫秒甚至几十毫秒,或者 max 偶尔出现一个异常大值,说明 Redis 在处理某些请求时卡住了。

排查从几个固定方向切入:慢命令、大 key、热点 key、fork 抖动、磁盘 fsync。每个方向都有对应的查看命令,按这个顺序讲。

一、缓存三大问题:穿透、击穿、雪崩

这三个问题经常被混淆,但成因不同:

问题发生了什么处理方向
缓存穿透查询的数据本来就不存在,缓存永远不命中,每次都查后端空值缓存、参数校验、布隆过滤器
缓存击穿某个热点 key 过期,瞬间大量请求同时回源互斥回源、后台定时刷新热点、逻辑过期
缓存雪崩大量 key 同时过期或 Redis 整体不可用随机 TTL、分批预热、降级限流

缓存穿透:数据库里没有这条记录,缓存也不会存它。攻击者或 bug 可以用不存在的 ID 反复查询,每次请求都穿透缓存打到数据库。处理方式是缓存一个短期的"空结果":

bash
# 查不到的数据也缓存一个空标记,短 TTL,防止同一个不存在的 ID 反复打库
redis-cli SET cache:user:404 '__NULL__' EX 60

布隆过滤器是另一种处理方式——在查缓存之前先问布隆过滤器"这个 key 可能存在吗",如果返回"不存在"就直接返回,不用查缓存也不用查数据库。

缓存击穿:热点 key 过期瞬间,大量请求同时发现缓存没了,一起去查数据库重建。用 Redis 锁让只有一个请求回源,其他等结果:

bash
lock_value="$(uuidgen)"
if redis-cli SET lock:rebuild:user:1001 "$lock_value" NX EX 10 | grep -q OK; then
  # 拿到锁,负责回源重建缓存
  echo "rebuild cache from database"
else
  # 没拿到锁,说明有别的请求在重建,短暂等待或走降级逻辑
  echo "waiting for cache rebuild"
fi

锁的过期时间要覆盖回源最大耗时。如果回源本身就很慢(比如查一张大表),锁过期后重建还没完成,第二个请求拿到新锁也会回源——这就是缓存的"击穿传导"。

缓存雪崩:大量 key 同时过期,或者 Redis 挂了,大量请求直接冲击后端。打散 TTL 是基础操作;Redis 主从或 Cluster 可以降低单点不可用的风险;应用侧的限流和降级是最后一道防线。

三种问题可以同时出现——比如 Redis 挂了导致雪崩,重启后热点 key 又集中过期造成击穿。处理时要分清是哪个环节先出问题。

二、大 key

bigkey 是指 value 很大或成员很多的 key——不是按字节绝对大小定义,而是看对操作的影响。一个 List 有 100 万条元素、一个 Hash 有 10 万个 field、一个 String 存了几 MB 的 JSON,都可以算 bigkey。

大 key 的风险有几个:阻塞主线程(删除大集合、HGETALL 大 Hash、LRANGE 0 -1 大 List 都是 O(n)——操作时间和元素数量成正比,元素越多越慢);网络带宽(一次返回大量数据,网卡被打满);内存不均衡(Cluster 中单个 slot 占过大,迁移慢);复制和持久化(fork 时的写时复制压力更大)。

扫描大 key:

bash
redis-cli --bigkeys

这个命令通过抽样统计来估算每种数据类型中最大的 key。它是抽样的,不是全量精确的,且扫描过程中会消耗 CPU。适合低峰巡检,不适合高峰期跑。

查看单个 key 的内存占用:

bash
redis-cli MEMORY USAGE big:key

删除大 key 时,DEL 在主线程释放内存——如果 key 里有很多元素或很大 value,主线程会阻塞在这个操作上。Redis 4.0 之后用 UNLINK 异步删除——标记删除后后台线程释放内存,主线程几乎不受影响:

bash
redis-cli UNLINK big:key    # 异步删除,不阻塞主线程

生产环境中删除大 key 的正确做法:不要直接 DEL。先用 UNLINK 异步删除;如果 Redis 版本低于 4.0 不支持 UNLINK,分批删除——Hash 用 HSCAN + HDEL,Set 用 SSCAN + SREM,List 用 LTRIM 从两端裁剪。一次性删除一个百万级元素的 key 可能阻塞主线程几秒甚至十几秒。

三、热 key

热 key 是访问特别集中的 key。它让单个 Redis 实例或 Cluster 分片成为瓶颈。表现是:单实例 CPU 高但 QPS 并不特别大(请求集中少数命令)、某个 Cluster 分片的连接数和负载明显高于其他分片。

检测热 key,Redis 自身提供的工具有限:

bash
redis-cli --hotkeys   # 需要 maxmemory-policy 设为 LFU 相关策略才有效

MONITOR 可以看实时命令流,但生产环境谨慎使用——MONITOR 把每条命令都打印出来,高流量时会拖慢 Redis 并且暴露敏感数据。更常见的排查方式是从应用日志、代理层统计或客户端侧采样。

处理热 key 的常见方向:本地缓存(应用内存,读多写少的热点配置用)、key 拆分(计数或集合类的热点,随机路由到 key:0~key:N)、加随机过期(避免过期时集中回源)、预热(发布或活动前把热点数据提前写进去)、限流(回源压力太大时保护后端)。

四、慢日志

Redis 慢日志记录执行时间超过阈值的命令。注意:记录的是命令实际执行时间,不包括网络传输和客户端排队。

conf
slowlog-log-slower-than 10000   # 10ms,单位微秒。默认 10000(=10ms),0 表示记录所有命令,负数表示不记录
slowlog-max-len 128             # 最多保留 128 条。默认 128,队列满后最旧的记录被丢弃

slowlog-log-slower-than 设为 0 会让每条命令都进慢日志——性能开销和日志量都很大,只适合临时排查。slowlog-max-len 只影响内存中保留的记录数,不影响性能。

查看:

bash
redis-cli SLOWLOG GET 10    # 最近 10 条慢命令
redis-cli SLOWLOG LEN        # 当前慢日志条数

常见出现在慢日志里的命令类型:

命令为什么慢
KEYS * 或带通配的 KEYS遍历整个键空间,O(n)
HGETALL 大 Hash返回所有 field-value,O(n)
SMEMBERS 大 Set返回所有成员,O(n)
LRANGE 0 -1 大 List读取整个列表,O(n)
大 key 的 DEL主线程释放大量内存
SINTER/SUNION 大集合运算量和集合大小成正比
ZRANGE 大 Sorted Set遍历跳表

慢日志里同一类命令反复出现,才是需要追溯的问题——偶发一条可能是维护操作或一次性批量任务。同一类命令每次都慢,说明访问模式或 key 设计有问题。

五、Pipeline

Pipeline 让客户端一次发送多条命令,再批量读回响应——减少网络往返次数(RTT)。适合大量独立的小命令:

bash
printf "SET k1 v1\r\nSET k2 v2\r\nSET k3 v3\r\n" | redis-cli --pipe

Pipeline 不是事务——中间某条命令失败不会回滚前面的,后续命令仍会继续执行。批量写入时要控制每批的大小:一批塞太多命令会让 Redis 的输出缓冲区膨胀,客户端也可能因为一次接收太多响应而超时。

六、延迟排查

客户端侧延迟探测:

bash
redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli --latency-history -h 127.0.0.1 -p 6379   # 持续记录延迟变化

Redis 内部的延迟事件分析:

bash
redis-cli LATENCY LATEST    # 最近延迟事件
redis-cli LATENCY DOCTOR    # 延迟诊断报告

延迟变高时,几个方向要同时看:慢日志里有什么命令在执行、INFO persistence 里是否在进行 BGSAVE/AOF 重写(fork 导致的抖动)、磁盘 iostat -x 1await 是否异常(AOF fsync 卡住)、INFO statsevicted_keys 是否在增长(淘汰消耗 CPU)。Redis 延迟很少是"Redis 本身变慢了",通常是某个具体操作在抢占 CPU 或磁盘——顺着这几个方向查,基本能定位到根因。

七、排查延迟的常见误区

误区一:看到延迟高就加从库分担读。 如果延迟的根因是慢命令阻塞主线程(比如 KEYS *HGETALL 大 Hash),加从库没用——复制流里同样有这条慢命令,从库一样会卡。先查慢日志,确认不是命令本身的问题。

误区二:看到 evicted_keys 增长就加内存。 evicted_keys 增长只说明内存触顶了,但根因可能是大量 key 没设过期时间在堆积、也可能是某个大 key 突然膨胀。先用 --bigkeys 扫一遍,再决定是加内存还是清理数据。

误区三:碎片率高就重启。 重启确实能重置碎片率,但重启期间 Redis 不可用(AOF/RDB 恢复需要时间)。Redis 4.0 之后支持 MEMORY PURGE 尝试手动回收碎片,影响比重启小得多。如果碎片率长期 > 1.5 且持续上升,再考虑重启。