Appearance
内存与性能
Redis 性能问题通常从几个方向切进去:内存用到了多少、淘汰策略是什么、有没有大 key 或热 key、慢日志里有什么命令、缓存有没有挡住下游压力、以及系统层的 CPU/磁盘/网络是否正常。
一、内存分析
内存总量视角
bash
redis-cli INFO memory关键字段:
| 字段 | 含义 |
|---|---|
used_memory | Redis 分配器统计的已用内存(不含内存碎片) |
used_memory_rss | 操作系统看到的该进程实际占用的物理内存(含碎片) |
mem_fragmentation_ratio | used_memory_rss / used_memory,大于 1 表示有碎片 |
maxmemory | 配置的内存上限,0 表示不限制 |
used_memory 和 used_memory_rss 不一样是正常的。Redis 释放 key 后内存不一定立刻还给操作系统——jemalloc 会保留一部分内存池以备再用。mem_fragmentation_ratio 长期大于 1.5 说明碎片比较严重,需要结合 Redis 版本和数据变更模式来分析。如果碎片率一直在涨,重启实例可以重置(但重启期间不可用,且 AOF/RDB 恢复需要时间)。
key 空间视角
bash
redis-cli INFO keyspace输出类似:
text
db0:keys=120000,expires=90000,avg_ttl=3600000keys—— 当前 DB 里的 key 总数expires—— 其中设置了过期时间的 key 数量avg_ttl—— 这些带过期时间的 key 的平均剩余存活时间(毫秒)
如果 keys 很大但 expires 很小,说明大部分 key 没有过期时间。这些永久 key 可能是配置数据、计数器等有意为之的,也可能是遗忘的缓存——后者会慢慢消耗内存。
二、maxmemory 和淘汰策略
maxmemory 是 Redis 使用内存的上限。达到上限后,maxmemory-policy 决定怎么处理新写入:
| 策略 | 行为 | 适合什么 |
|---|---|---|
noeviction | 不淘汰,写入直接报错 | 不能接受任何数据被自动删除 |
allkeys-lru | 所有 key 中淘汰最近最少使用的 | 纯缓存用途,所有数据都可以丢 |
volatile-lru | 只在设了过期时间的 key 中淘汰 | 永久数据 + 缓存混合 |
allkeys-random | 所有 key 中随机淘汰 | 访问模式均匀的缓存 |
volatile-ttl | 淘汰 TTL 最短的 key | 希望保留长 TTL 的缓存 |
查看淘汰是否在发生:
bash
redis-cli INFO stats | grep evicted_keysevicted_keys 持续增长说明内存已经触顶,Redis 在按策略踢 key。缓存场景里这不一定不正常——allkeys-lru 下这是预期行为。但如果 Redis 里存的是业务状态(计数器、会话),evicted_keys > 0 就是在丢数据,需要扩内存或者排查 key 是否有不合理的增长。
注意区分 evicted_keys 和 expired_keys:前者是因为内存满了被 maxmemory 策略踢掉的,后者是因为 TTL 到期被过期机制删掉的。统计口径完全不同。
三、过期策略的实际行为
Redis 删除过期 key 靠两种机制配合:
- 惰性删除:访问 key 时检查,过期就删
- 定期删除:后台每秒多次随机抽样,删掉其中过期的
这意味着:key 到期后不一定会被立刻删除,没被访问到的过期 key 会等到定期扫描才清理。大量 key 集中在同一秒过期时(比如整点设置的缓存),定期删除可能短时间消耗明显 CPU。给 TTL 加一点随机值可以打散过期时间:
bash
# 基础 300 秒,加 0-60 秒随机抖动,避免大量 key 同时过期
ttl=$((300 + RANDOM % 61))
redis-cli SET cache:user:1001 "$value" EX "$ttl"四、缓存穿透、击穿和雪崩
这三个问题经常被混淆,但成因不同:
| 问题 | 发生了什么 | 处理方向 |
|---|---|---|
| 缓存穿透 | 查询的数据本来就不存在,缓存永远不命中,每次都查后端 | 空值缓存、参数校验、布隆过滤器 |
| 缓存击穿 | 某个热点 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 又集中过期造成击穿。处理时要分清是哪个环节先出问题。
五、bigkey
bigkey 是指 value 很大或成员很多的 key——不是按字节绝对大小定义,而是看对操作的影响。一个 List 有 100 万条元素、一个 Hash 有 10 万个 field、一个 String 存了几 MB 的 JSON,都可以算 bigkey。
bigkey 的风险:
| 风险 | 具体影响 |
|---|---|
| 阻塞主线程 | 删除大集合、HGETALL 大 Hash、LRANGE 0 -1 大 List——都是 O(n) |
| 网络带宽 | 一次返回大量数据,网卡被打满 |
| 内存不均衡 | Cluster 中单个 slot 占过大,迁移慢 |
| 复制和持久化 | fork 时的写时复制压力更大 |
扫描 bigkey:
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
热 key 是访问特别集中的 key。它让单个 Redis 实例或 Cluster 分片成为瓶颈。表现是:单实例 CPU 高但 QPS 并不特别大(请求集中少数命令)、某个 Cluster 分片的连接数和负载明显高于其他分片。
检测热 key,Redis 自身提供的工具有限:
bash
redis-cli --hotkeys # 需要 maxmemory-policy 设为 LFU 相关策略才有效MONITOR 可以看实时命令流,但生产环境谨慎使用——MONITOR 把每条命令都打印出来,高流量时会拖慢 Redis 并且暴露敏感数据。更常见的排查方式是从应用日志、代理层(ProxySQL/Twemproxy)统计或客户端侧采样。
处理热 key 的常见方向:
| 做法 | 适用场景 |
|---|---|
| 本地缓存(应用内存) | 读多写少的热点配置,变化频率低 |
key 拆分(如 key:0 ~ key:N) | 计数或集合类的热点,随机路由到不同 key |
| 加随机过期 | 避免过期时集中回源 |
| 预热 | 发布或活动前把热点数据提前写进去 |
| 限流 | 回源压力太大时保护后端 |
七、慢日志
Redis 慢日志记录执行时间超过阈值的命令。注意:记录的是命令实际执行时间,不包括网络传输和客户端排队。
conf
slowlog-log-slower-than 10000 # 10ms,单位微秒
slowlog-max-len 128 # 最多保留 128 条查看:
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 | 遍历跳表 |
八、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 或磁盘。