Appearance
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 --pipePipeline 不是事务——中间某条命令失败不会回滚前面的,后续命令仍会继续执行。批量写入时要控制每批的大小:一批塞太多命令会让 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 1 的 await 是否异常(AOF fsync 卡住)、INFO stats 里 evicted_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 且持续上升,再考虑重启。