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 的 Key | expires 巨大、内存不降 | 打散 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 驱逐相关配置项
| 配置 | 默认 | 作用 |
|---|---|---|
maxmemory | 0(不限制) | 内存上限 |
maxmemory-policy | noeviction | 淘汰策略 |
maxmemory-samples | 5 | LRU/LFU 采样数 |
maxmemory-eviction-tenacity | 10(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_ratio | RSS / 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/s | 5/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 内存治理四件套
- 设上限:
maxmemory按物理内存 70% 配置 - 定策略:缓存
allkeys-lru/lfu,可靠存储noeviction - 管过期:TTL 打散、
hz调优、过期键监控 - 清碎片:
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 生产稳定性的根基,本文核心要点回顾:
- 过期删除是"懒惰"的:惰性删除在访问时触发,定期删除靠
hz兜底,两者结合才能既省 CPU 又不积压 - 驱逐策略是"兜底":
noeviction保数据、allkeys-lru保缓存、allkeys-lfu保热点,选型要与业务语义匹配 - 碎片要主动治理:
activedefrag+ 合理的阈值配置,让 jemalloc 的碎片率保持在健康区间 - 监控要闭环:
INFO memory、MEMORY DOCTOR、淘汰速率与碎片率告警,缺一不可 - 大 Key 是放大器:大 Key 的过期、淘汰、删除都会放大阻塞风险,内存治理与 Key 治理必须联动
给内存设上限、给数据设 TTL、给策略定语义、给碎片设整理——这四件事做好,Redis 的内存就是可控的;任何一件缺失,内存失控只是时间问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。