内存、淘汰与碎片
used_memory 是 Redis 自己记账的内存,used_memory_rss 是操作系统实际给这个进程的内存,两者之间的差额就是碎片,或者已经释放但没有归还给系统的内存。本章按“涨得快、涨得满、涨得虚”三种情况分别给出判读口径与处置顺序。
先看清这几个数
redis-cli info memory
redis-cli info memory | grep -E 'used_memory_human|used_memory_rss_human|used_memory_peak_human|maxmemory_human|maxmemory_policy|mem_fragmentation_ratio|mem_clients_normal'
| 字段 | 含义 | 判读要点 |
|---|---|---|
used_memory | 分配器记账的数据 + 内部开销 | 持续单向增长且业务量平稳时,优先怀疑内存泄漏 |
used_memory_rss | 操作系统视角的常驻内存 | 与 used_memory 差距过大说明碎片或未归还 |
used_memory_peak | 历史峰值 | 峰值远高于当前值时,说明曾有大批量写入或溢出 |
mem_fragmentation_ratio | rss / used_memory | 1.0 ~ 1.5 正常;大于 1.5 有碎片;小于 1 说明用了 swap |
mem_clients_normal | 普通客户端缓冲占用 | 突然升高说明慢客户端或大 value 写入 |
mem_not_counted_for_evict | 不计入淘汰的内存(如 AOF 缓冲) | 内存满了却淘汰不动时,来看这一项 |
allocator_frag_ratio | 分配器自身的碎片率 | 用于区分“分配器碎片”与“操作系统未回收” |
情况一:涨得快——先分清是数据还是缓冲
redis-cli info memory
redis-cli info clients
redis-cli client list | sed 's/ /\n/g' | grep '^omem=' | sort -t= -k2 -nr | head -10
redis-cli --bigkeys -i 0.1
redis-cli memory doctor
MEMORY DOCTOR 给出的是提示性建议,比如“峰值内存远大于当前内存”“碎片率偏高”“大量内存被客户端缓冲占用”,它不是精确诊断,要顺着提示再去验证。判读顺序:客户端缓冲暴涨 → 查慢客户端;--bigkeys 有异常 → 走第 24 章的拆分;都没异常但曲线持续上升 → 怀疑 key 只增不删(缺少 TTL)。
情况二:涨得满——淘汰策略怎么选
| 策略 | 淘汰范围 | 适合场景 | 风险 |
|---|---|---|---|
noeviction | 不淘汰,写入直接返回 OOM 错误 | 当作持久存储、绝不允许丢数据的场景 | 内存满后写入全部失败,是硬故障 |
allkeys-lru | 所有 key 按最近最少使用淘汰 | 纯缓存场景的默认选择 | 长时间未被访问但重要的数据会被删 |
allkeys-lfu | 所有 key 按访问频率淘汰 | 热点集中、有长尾访问的业务 | 新 key 需要累积计数,短期可能被误删 |
allkeys-random | 所有 key 随机淘汰 | 几乎不用 | 命中率随机波动 |
volatile-lru / volatile-lfu | 只淘汰设置了过期时间的 key | 同一实例混存持久数据与缓存 | 可丢数据若没设 TTL,会退化成 noeviction |
volatile-random | 带 TTL 的 key 随机淘汰 | 极少使用 | 同上 |
volatile-ttl | 优先淘汰剩余存活时间最短的 | TTL 语义本身就代表重要性的场景 | TTL 设计不合理时淘汰效果差 |
选择建议:纯缓存用 allkeys-lfu(热点明显时优于 LRU),混存场景用 volatile-lfu 且缓存类 key 必须带 TTL。不要用 noeviction 当缓存——那等于把 Redis 变成一个随时会写失败的存储。
evicted_keys 的曲线判读:
| evicted_keys 曲线 | 含义 | 动作 |
|---|---|---|
| 稳定为 0 | 容量充足 | 保持巡检 |
| 偶发小幅增长 | 已接近水位 | 观察命中率,准备扩容 |
| 持续增长且命中率下降 | 缓存正在删掉自己有用的数据 | 立即扩容,同时压缩 value、缩短 TTL、拆分实例 |
| 持续增长但命中率不变 | 淘汰的是冷数据 | 仍要规划容量,不要当成正常现象 |
情况三:涨得虚——碎片与操作系统行为
redis-cli info memory | grep -E 'mem_fragmentation_ratio|allocator_frag_ratio|active_defrag_running'
redis-cli config set activedefrag yes
redis-cli config set active-defrag-ignore-bytes 100mb
redis-cli config set active-defrag-threshold-lower 10
- 碎片率持续大于 1.5 且 CPU 有余量时,开启
activedefrag在线整理;它本身消耗 CPU,高负载实例上要谨慎,一次只在一个节点开。 - 碎片率小于 1 通常意味着部分内存在 swap 里,这是性能杀手:先解决物理内存,再谈其他参数。
- 负载突增导致的碎片,往往在业务回落后自然恢复;只有长期不降才需要主动整理或重启。
- 重启是消灭碎片最彻底的方法,但会丢失未持久化数据并可能触发全量同步,必须放在低峰并有预案。
过期删除机制与 expired_keys
- 惰性删除:key 被访问时才判断是否过期,过期则删除。
- 定期删除:每秒若干次(受
hz影响)随机抽取一批带 TTL 的 key,删除其中已过期的;若过期比例超过 25%,同一轮继续采样。 - 结论:大量 key 同一时刻过期会形成“过期风暴”——删除动作与业务命令争抢单线程 CPU,且过期 key 在被删除前仍占内存。
- 应对:TTL 加随机抖动(基准值 + 0 ~ 10% 随机偏移);批量预热时也把过期时间打散。
内存水位与扩容流程
| 水位(used_memory / maxmemory) | 处置 |
|---|---|
| 小于 70% | 正常,保持巡检 |
| 70% ~ 85% | 排查增长来源(大 key、无 TTL 的 key、客户端缓冲),准备扩容 |
| 大于 85% | 扩容;同时缩短非核心数据 TTL、清理无用 key、给 fork 留出余量 |
| 达到 100% 且策略为 noeviction | 写入已经失败,属于线上故障,立即扩容或紧急清理 |
扩容流程:新增节点 → 加入集群或建立主从 → 迁移槽位或切换主库 → 观察一周 → 下线旧节点。Cluster 用 redis-cli --cluster reshard,主从架构则新增从库后提升为主库并切换客户端地址。maxmemory 建议设为物理内存的 70% ~ 80%,因为 fork 期间的写时复制可能额外占用接近一份数据的内存。
内存暴涨的四种典型来源
| 来源 | 特征 | 验证命令 | 处置 |
|---|---|---|---|
| 客户端输出缓冲 | mem_clients_normal 阶梯上升 | CLIENT LIST 看 omem | 限制输出缓冲并断开慢客户端 |
| 大 key 批量写入 | used_memory 呈台阶式上涨 | redis-cli --bigkeys -i 0.1 | 拆分大 key,分批写入 |
| 缺少 TTL 的数据 | 曲线单调上升,expired_keys 几乎不增长 | 抽样检查 key 的 TTL | 补 TTL,清理无用 key |
| 复制缓冲与 AOF 缓冲 | mem_not_counted_for_evict 上升,淘汰不动 | info replication、info persistence | 调 repl-backlog-size,调整 AOF 策略 |
# 小样本抽查没有过期时间的 key(TTL 返回 -1 表示永不过期,命令较慢,只做抽样)
redis-cli --scan --pattern 'cache:*' | head -500 | while read -r k; do
[ "$(redis-cli ttl "$k")" = "-1" ] && echo "$k"
done
redis-cli memory purge # 仅把空闲内存还给操作系统,不释放任何数据
常见误判
| 误判 | 事实 |
|---|---|
| 碎片率大于 1.5 就要重启 | 业务回落后碎片常自行下降,先观察一两个周期 |
内存高就调大 maxmemory | 内存不足是物理资源问题,调参数只会把 OOM 推迟 |
evicted_keys 为 0 就没有内存问题 | noeviction 下不淘汰,但写入会直接失败 |
MEMORY PURGE 能解决内存不足 | 它只归还空闲内存,数据占用一点没减 |
预防与巡检
- 非核心数据必须有 TTL,禁止永久 key 无限堆积;每周对无 TTL 的 key 做一次抽查。
- 监控
used_memory、used_memory_rss、evicted_keys、expired_keys与碎片率五条曲线,并做周同比。 - 大促或压测前评估峰值内存,把 fork 与 AOF 重写的额外开销算进去,不要顶到
maxmemory才扩容。 - 内存增长异常时第一时间保留现场(
info memory、--bigkeys结果、CLIENT LIST),再动手清理。
小结:把内存问题分成涨得快、涨得满、涨得虚三类分别处理——先分清是数据增长还是客户端缓冲,再按业务语义选淘汰策略并盯住 evicted_keys 曲线,最后才处理碎片与 swap;maxmemory 留出 20% ~ 30% 余量,过期时间加随机抖动避免过期风暴,内存水位超过 85% 就开始扩容而不是等到写入失败。