Skip to content

内存与性能

Redis 性能问题通常从几个方向切进去:内存用到了多少、淘汰策略是什么、有没有大 key 或热 key、慢日志里有什么命令、缓存有没有挡住下游压力、以及系统层的 CPU/磁盘/网络是否正常。

一、内存分析

内存总量视角

bash
redis-cli INFO memory

关键字段:

字段含义
used_memoryRedis 分配器统计的已用内存(不含内存碎片)
used_memory_rss操作系统看到的该进程实际占用的物理内存(含碎片)
mem_fragmentation_ratioused_memory_rss / used_memory,大于 1 表示有碎片
maxmemory配置的内存上限,0 表示不限制

used_memoryused_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=3600000
  • keys —— 当前 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_keys

evicted_keys 持续增长说明内存已经触顶,Redis 在按策略踢 key。缓存场景里这不一定不正常——allkeys-lru 下这是预期行为。但如果 Redis 里存的是业务状态(计数器、会话),evicted_keys > 0 就是在丢数据,需要扩内存或者排查 key 是否有不合理的增长。

注意区分 evicted_keysexpired_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 --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 或磁盘。