过期键淘汰与内存回收深入:expire cycle、驱逐策略与碎片整理

Redis 过期键淘汰与内存回收深入:过期删除策略(惰性/定期/秒与毫秒级)、主动过期扫描(expire cycle/hz)、内存淘汰策略(noeviction/allkeys-lru/volatile-lru/lfu)、maxmemory 配置、内存碎片整理(activedefrag)、内存监控(INFO memory/MEMORY DOCTOR/RDI)、调优案例与驱逐相关坑

Redis 是内存数据库,内存是它最稀缺的资源。两个看似简单的问题——过期键什么时候真正被删除、内存满了之后如何腾挪——直接决定了 Redis 的稳定性与性能。

很多人以为设置了 EXPIRE 到期后键就立刻消失了,其实不然:过期键的实际删除依赖惰性删除与定期扫描;内存满时,maxmemory-policy 决定谁先被淘汰;而在 jemalloc 分配器下,频繁的写入删除还会留下大量内存碎片。本文深入这三块机制,配合真实配置与监控命令,帮助你建立完整的内存回收认知。


一、过期删除策略:惰性删除与定期删除

1.1 过期键的两种删除方式

策略触发时机优点缺点
惰性删除访问该 Key 时检查是否过期开销小,不额外占用 CPU过期键不访问就一直占内存
定期删除周期性的 expire cycle 主动扫描及时回收,内存可控有 CPU 开销,需限时

Redis 采用惰性删除 + 定期删除结合的策略:正常读写路径上做惰性检查;后台按 hz 频率做定期扫描兜底。

# 查看惰性删除的累计次数(随访问触发)
redis-cli INFO stats | grep -E "expired_keys|evicted_keys"
# expired_keys:126738
# evicted_keys:0

# 查看各库 Key 数量与有过期时间的 Key 数量
redis-cli INFO keyspace
# db0:keys=189231,expires=45210,avg_ttl=86400

INFO keyspace 中的 expires 指"设置了过期时间"的 Key 数量,avg_ttl 是这些 Key 的平均剩余寿命。expired_keys 是自启动以来已删除的过期键总数。

1.2 惰性删除的实现路径

当客户端执行 GET、HGETALL 等读命令时,Redis 会先检查键的过期时间(expire 字段),若已过期则先删除再返回 nil:

# 设一个 5 秒后过期的 Key
redis-cli SET session:token "abc123" EX 5
redis-cli TTL session:token
# (integer) 5

# 5 秒后访问,直接返回 nil,键已被惰性删除
redis-cli GET session:token
# (nil)
redis-cli EXISTS session:token
# (integer) 0

1.3 秒与毫秒级过期

Redis 内部统一用毫秒级时间戳记录过期时间,对外提供两套命令:

命令精度示例
EXPIRE key seconds秒EXPIRE k 3600
PEXPIRE key ms毫秒PEXPIRE k 3600000
EXPIREAT key unix-time秒时间戳EXPIREAT k 1800000000
PEXPIREAT key unix-ms毫秒时间戳PEXPIREAT k 1800000000000
SET key val EX/PX秒/毫秒SET k v PX 1000
# 时间戳方式(适合"当天 23:59:59 过期"这种业务)
redis-cli EXPIREAT coupon:1001 $(date -d '23:59:59' +%s)

# 查看剩余毫秒
redis-cli PTTL coupon:1001

二、主动过期扫描:expire cycle 机制

2.1 定期删除怎么跑

Redis 主循环按 hz(默认 10,即每秒 10 次)周期性调用 activeExpireCycle,每次从 expires 字典中随机抽样一批 Key,删除其中已过期的,同时限制每次扫描的耗时与样本数,避免影响正常命令处理。

# redis.conf
# hz: 后台任务执行频率(范围 1~500,默认 10)
hz 10

hz 越大,过期键回收越及时,但 CPU 占用越高。Redis 7.0 起,hz 最大值提高到 500。对缓存量大的实例,可适当提高到 20~50 观察 CPU 与过期键积压情况。

2.2 expire cycle 的执行细节

activeExpireCycle 的核心是时间预算:每次运行不超过固定耗时(约 25% 的事件循环时间),且单次最多抽样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP(默认 20)个 Key。它的具体行为:

  • 在 db->expires 字典中随机采样
  • 删除采样到的过期 Key,循环直到采样到未过期键
  • 若一轮删除比例高,会连续多轮运行
  • 每轮有时间上限,避免阻塞主线程
# 观察过期键是否积压(expired_keys 增长缓慢 + keyspace expires 居高不下)
# 说明定期删除跟不上过期速率,需要提高 hz 或检查大 Key
redis-cli INFO stats | grep expired_keys
redis-cli INFO keyspace

2.3 过期键积压的典型原因

原因表现对策
一次性写入大量同 TTL 的 Keyexpires 巨大、内存不降打散 TTL,分批过期
hz 过低过期键回收慢提高 hz
大 Key 过多每次删除耗时长先治理大 Key
副本延迟删除主库已删、副本仍占内存主从版本对齐,开启 lazyfree

注意:主从复制中,过期键的删除由主库发起。主库删除后向副本发送 DEL,副本不主动跑 expire cycle。若主从网络延迟或主库负载高,副本上的过期键可能"看起来还在"。


三、内存淘汰策略:maxmemory-policy

3.1 八种淘汰策略

当 used_memory 达到 maxmemory 上限后,Redis 按 maxmemory-policy 决定哪些键被淘汰:

策略含义使用建议
noeviction不淘汰,写入直接报 OOM 错误用作消息队列/需绝对不丢数据的场景
allkeys-lru全体键按 LRU 淘汰纯缓存场景首选
allkeys-random全体键随机淘汰访问均匀、无热点
volatile-lru仅带 TTL 的键按 LRU 淘汰只淘汰有过期时间的键
volatile-ttl优先淘汰 TTL 最小的键极少用
volatile-random仅带 TTL 的键随机淘汰极少用
allkeys-lfu全体键按 LFU 淘汰访问频率差异大、有热点
volatile-lfu仅带 TTL 的键按 LFU 淘汰同上但限定带 TTL 键

3.2 动态配置与验证

# 设置内存上限为 4GB
redis-cli CONFIG SET maxmemory 4gb
redis-cli CONFIG GET maxmemory
# 1) "maxmemory" 2) "4294967296"

# 设置淘汰策略
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli CONFIG SET maxmemory-policy noeviction

# 查看当前策略
redis-cli CONFIG GET maxmemory-policy

# 持久化到配置文件
redis-cli CONFIG REWRITE
# 验证:noeviction 下写满会报错
redis-cli SET demo:001 value1
# (error) OOM command not allowed when used memory > 'maxmemory'

3.3 LRU/LFU 的采样近似

Redis 的 LRU/LFU 是采样近似而非精确算法,通过 maxmemory-samples(默认 5)控制:每次从候选键中随机采样 N 个,淘汰其中最久未使用/频率最低的。

# 采样数越大淘汰越精确,CPU 开销越大;缓存场景推荐 10
maxmemory-samples 10
# 查看 Key 的近似访问频率(需 LFU 策略)
redis-cli OBJECT FREQ user:1001
# (integer) 88
采样数淘汰精确度CPU 开销建议
5(默认)中低多数场景
10较高中缓存命中率敏感场景
50高高追求极致命中率

3.4 驱逐相关配置项

配置默认作用
maxmemory0(不限制)内存上限
maxmemory-policynoeviction淘汰策略
maxmemory-samples5LRU/LFU 采样数
maxmemory-eviction-tenacity10(Redis 7.0+)驱逐"韧性",越大越坚决

四、maxmemory 配置与内存管理

4.1 配置原则

  • 不要设为 0:生产必须设置 maxmemory,否则内存暴涨会拖垮系统甚至 OOM
  • 预留余量:物理内存须大于 maxmemory + RDB/AOF 缓冲区 + 复制缓冲区 + 系统开销,建议 maxmemory ≤ 物理内存 × 70%
  • 集群分片各自设置:Cluster 每个节点独立配置,保证各分片水位一致
# 8GB 物理机示例
maxmemory 5gb
maxmemory-policy allkeys-lru
maxmemory-samples 10

4.2 内存组成部分

Redis 内存由三部分构成,理解它们才能正确设置 maxmemory:

部分含义说明
数据内存Key + Value + 内部结构可被淘汰回收
缓冲内存复制 backlog、客户端输出缓冲、AOF 缓冲不可淘汰,需预留
元数据开销字典、过期字典、客户端对象规模相关

复制积压缓冲 repl-backlog-size(默认 1MB)与主从偏移差相关,缓存量大、写入频繁时需调大;客户端输出缓冲(client-output-buffer-limit)用于慢消费场景。设置 maxmemory 时必须把这些"不可淘汰内存"计算在内,否则会过早触发淘汰。

4.3 内存增长的监控

# 查看内存组成明细
redis-cli INFO memory | grep -E "used_memory_human|used_memory_dataset|used_memory_overhead|maxmemory_human"
# used_memory_human:3.00G
# used_memory_dataset:2324357120
# used_memory_overhead:896868352

也可用 redis-cli --stat 实时刷新整体内存与客户端数。


五、内存碎片整理:activedefrag

5.1 碎片从哪里来

Redis 使用 jemalloc 分配内存,频繁的写入、删除、过期、淘汰会让内存页出现"小洞"。used_memory(逻辑占用)与 used_memory_rss(物理占用)的比值 mem_fragmentation_ratio 反映了碎片程度。

碎片率区间含义处理
< 1.0使用了 swap 或共享内存检查是否过量使用交换
1.0 ~ 1.5健康无需处理
1.5 ~ 2.0有碎片开启 activedefrag
> 2.0严重碎片整理 + 评估重启(RDB 重载)
redis-cli INFO memory | grep -E "mem_fragmentation_ratio|used_memory_rss|used_memory"
# mem_fragmentation_ratio:1.78

5.2 activedefrag 配置

Redis 4.0+ 提供后台主动碎片整理,在碎片率超过阈值时自动移动/合并内存块:

# redis.conf
# 开启主动碎片整理
activedefrag yes

# 碎片整理触发条件(字节数或比率,任一满足即开始)
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100

# 整理过程中 CPU 占用控制(百分比)
active-defrag-cycle-min 5
active-defrag-cycle-max 75

# 每次最多扫描的 Hash/List/Set/ZSet 字段数
active-defrag-max-scan-fields 1000
# 动态开启
redis-cli CONFIG SET activedefrag yes

active-defrag-threshold-lower 10 表示碎片率 > 10% 开始整理;upper 100 表示碎片率 > 100%(即 ratio 2.0)时全力整理。cycle-max 75 控制整理最多占用 75% 的 CPU 时间片,防止整理本身影响服务。

5.3 碎片整理的观察

# 查看整理效果:碎片率应逐渐回落
watch -n 5 'redis-cli INFO memory | grep mem_fragmentation_ratio'

Redis 7.2+ 还提供 INFO memory 中的 active_defrag_running、active_defrag_hits 等字段,用于观察整理进度与命中率。


六、内存监控:INFO memory、MEMORY DOCTOR 与 RDI

6.1 MEMORY 命令家族

命令用途
INFO memory内存总览
MEMORY STATS更细的内存统计
MEMORY DOCTOR内存健康诊断与建议
MEMORY USAGE key单 Key 内存
MEMORY PURGE主动清理未使用的内存页
CONFIG RESETSTAT重置统计计数器
# 内存健康诊断
redis-cli MEMORY DOCTOR
# 典型输出: "Sam, I detected a few issues with your Redis instance memory implants:
# 1. High fragmentation detected, ...
# 2. Peak memory > current used memory, ..."

# 查看 RSS、碎片、峰值等关键项
redis-cli MEMORY STATS
# peak.allocated:4294967296
# total.allocated:3221225472
# fragmentation.ratio:1.78
# fragmentation.bytes:1290420480

6.2 INFO memory 关键字段

字段含义
used_memory逻辑内存占用(不含操作系统页对齐)
used_memory_rss物理内存占用(RSS)
used_memory_peak历史峰值
mem_fragmentation_ratioRSS / used_memory,碎片率
used_memory_dataset数据部分占用
used_memory_overhead缓冲与元数据开销
maxmemory配置的内存上限
# 例行巡检命令
redis-cli INFO memory | awk -F: '/used_memory_human|used_memory_rss|mem_fragmentation_ratio|used_memory_peak_human|maxmemory_human/ {print $1": "$2}'

6.3 RDI 与内存联动

RDI(Redis Data Integration)是 Redis 官方的实时数据集成工具(基于 CDC),用于数据库与 Redis 之间的实时同步。对内存治理而言,它的价值在于:

  • 冷热分层:通过 RDI 把 Redis 中的冷数据按策略回写数据库,降低 Redis 内存水位
  • 数据源治理:把高频变更的数据以增量方式同步进 Redis,避免全量重建产生内存尖峰
  • 可观测联动:RDI 的同步进度与 Redis 内存水位一起监控,及时发现同步异常导致的内存膨胀

简单说:RDI 解决"数据怎么进来、怎么出去",与 INFO memory 一起构成内存治理的"输入输出"闭环。对超大规模缓存集群,建议评估引入。


七、调优案例实战

7.1 案例一:缓存实例频繁触发淘汰

现象:evicted_keys 持续上涨,命中率下滑,INFO stats 中 keyspace_hits 占比下降。

排查:maxmemory-policy 为 volatile-lru,但多数缓存 Key 未设置 TTL(不带 expire),导致可淘汰池很小,新写入立即触发淘汰。

修复:业务侧所有缓存 Key 统一设置合理 TTL;策略改为 allkeys-lru 兜底;采样数提到 10。

项修复前修复后
命中率78%95%
evicted_keys 增速500/s5/s

7.2 案例二:大促期间内存碎片率飙升

现象:mem_fragmentation_ratio 达 2.3,used_memory 3GB 但 RSS 7GB,物理内存告警。

原因:大促大量写入删除 + 过期键成批淘汰,jemalloc 无法归还内存给 OS。

修复:开启 activedefrag yes,设置 active-defrag-threshold-lower 10、active-defrag-cycle-max 75;碎片率在 2 小时内从 2.3 降到 1.4。

# 若碎片整理效果不理想,可配合 MEMORY PURGE 主动释放
redis-cli MEMORY PURGE

7.3 案例三:noeviction 下写入报 OOM

现象:(error) OOM command not allowed when used memory > 'maxmemory',订单写入失败。

分析:该实例用作"可靠队列",noeviction 是为了不丢数据,但 maxmemory 设置过小,且大量已消费的 Stream/List 未清理。

修复:调大 maxmemory;对已 ACK 的 Stream 条目设置 XTRIM 裁剪;把队列与缓存拆分到不同实例。

关键经验:淘汰策略是"兜底",不是"日常"。长期依赖淘汰来腾内存,说明容量规划与 TTL 设计出了问题。


八、与驱逐相关的常见坑

8.1 常见坑汇总

坑后果规避
maxmemory 设为 0内存涨满 OOM 被系统 Kill生产必须设上限
volatile-lru 但无 TTL 键可淘汰池过小,误删新数据统一 TTL 或 allkeys-lru
大 Key 被淘汰淘汰过程阻塞主线程先治理大 Key
碎片率长期偏高RSS 虚高、内存告警activedefrag + 重启重载
hz 过低 + 海量短 TTL过期键积压、内存不降提高 hz、打散 TTL
主从版本不一致过期删除行为差异版本对齐
淘汰触发后主从不一致主库淘汰、副本未同步开启 lazyfree、监控偏移

8.2 大 Key 与淘汰的叠加效应

大 Key 被 LRU/LFU 淘汰时,释放内存是 O(N) 操作,同样阻塞主线程。应开启 lazyfree-lazy-eviction yes,让淘汰释放走异步线程,同时优先治理大 Key。

8.3 淘汰阈值与告警

生产必须为淘汰与碎片配置告警:用 Prometheus 采集 redis_evicted_keys_total 与 redis_mem_fragmentation_ratio(redis_exporter 提供),设置 rate(redis_evicted_keys_total[5m]) > 100 与 redis_mem_fragmentation_ratio > 2 两条规则,持续 5~10 分钟触发,避免瞬时抖动误报。


九、最佳实践与检查清单

9.1 内存治理四件套

  1. 设上限:maxmemory 按物理内存 70% 配置
  2. 定策略:缓存 allkeys-lru/lfu,可靠存储 noeviction
  3. 管过期:TTL 打散、hz 调优、过期键监控
  4. 清碎片:activedefrag + MEMORY DOCTOR 定期体检

9.2 生产检查清单

  • maxmemory 已设置且留有缓冲余量
  • maxmemory-policy 与业务语义匹配(缓存 vs 可靠存储)
  • 所有缓存 Key 设置了合理 TTL 并打散过期时间
  • hz 与过期键规模匹配(积压时调高)
  • activedefrag yes 且阈值合理
  • lazyfree-lazy-eviction/expire 已开启
  • 大 Key 已完成拆分(避免淘汰阻塞)
  • INFO memory / MEMORY DOCTOR 纳入巡检
  • 有淘汰速率与碎片率的告警规则
  • 每季度做一次内存体检并复盘
# 一条命令完成内存体检
redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss|mem_fragmentation_ratio|used_memory_peak_human|maxmemory_human"
redis-cli MEMORY DOCTOR

结语

内存回收是 Redis 生产稳定性的根基,本文核心要点回顾:

  1. 过期删除是"懒惰"的:惰性删除在访问时触发,定期删除靠 hz 兜底,两者结合才能既省 CPU 又不积压
  2. 驱逐策略是"兜底":noeviction 保数据、allkeys-lru 保缓存、allkeys-lfu 保热点,选型要与业务语义匹配
  3. 碎片要主动治理:activedefrag + 合理的阈值配置,让 jemalloc 的碎片率保持在健康区间
  4. 监控要闭环:INFO memory、MEMORY DOCTOR、淘汰速率与碎片率告警,缺一不可
  5. 大 Key 是放大器:大 Key 的过期、淘汰、删除都会放大阻塞风险,内存治理与 Key 治理必须联动

给内存设上限、给数据设 TTL、给策略定语义、给碎片设整理——这四件事做好,Redis 的内存就是可控的;任何一件缺失,内存失控只是时间问题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. Redis 向量检索实战:RediSearch、HNSW 与 Embedding 管道集成
  2. 缓存一致性终极方案:双删、binlog 订阅与最终一致性架构
  3. 高级数据结构实战:Bitmap、HyperLogLog、GEO、布隆过滤器与 Stream