Redis 内存管理与淘汰机制¶
一句话记忆口诀:
- 过期删除 = 惰性 + 定期——访问时懒删 + 每 100ms 随机抽 20 个兜底,两者都漏的靠淘汰策略擦屁股。
- 8 种淘汰策略记 2 个维度——范围(
allkeysvsvolatilevsnoeviction)× 算法(lru/lfu/random/ttl)。- 生产三件套:纯缓存
allkeys-lru、热点明显allkeys-lfu、缓存+持久化混用volatile-lru。- Redis 的 LRU 是近似 LRU——采样 N 个淘汰最旧的那个,
maxmemory-samples从 5 调 10 精度显著提升。- 碎片率 > 1.5 开
activedefrag,< 1.0 说明在用 Swap(最危险)。📖 边界声明:本文聚焦「Redis 如何管内存 + 什么时候淘汰 + 淘汰谁」,以下主题请见对应专题:
- 缓存穿透 / 击穿 / 雪崩的应用层方案(布隆过滤器、互斥锁、随机 TTL)→ 缓存三大问题
- 大 Key / 热 Key 的排查与拆分、缓存一致性工程实践 → 应用型问题
- RDB / AOF 重写对内存的瞬时放大效应(fork 写时复制)→ 持久化机制RDB与AOF
- Redis 的线程模型与命令处理链路 → 单线程模型与网络IO
1. 引入:为什么要关注内存管理?¶
Redis 是纯内存数据库,内存是最核心的资源。生产环境中常见的问题:
- OOM(内存溢出):Redis 内存打满,写入报错
OOM command not allowed when used memory > 'maxmemory' - 内存碎片率高:
used_memory_rss远大于used_memory,实际使用内存远超预期 - Key 过期不及时:大量过期 Key 未被清理,内存持续增长
理解内存管理,能帮助你:
- 合理配置
maxmemory和淘汰策略,避免 OOM - 排查内存碎片问题,降低内存占用
- 理解 Key 过期删除机制,避免内存泄漏
2. Redis 内存分配器:jemalloc¶
Redis 默认使用 jemalloc 作为内存分配器(而非 glibc 的 malloc)。
2.1 为什么选择 jemalloc?¶
| 对比项 | glibc malloc | jemalloc |
|---|---|---|
| 内存碎片率 | 较高 | 低(分级分配,减少碎片) |
| 多线程性能 | 一般 | 好(线程本地缓存) |
| 内存归还 | 慢 | 快(主动归还 OS) |
jemalloc 分级分配原理:将内存请求按大小分为多个级别(如 8B、16B、32B...),每次分配时向上取整到最近的级别,减少碎片。
2.2 内存相关指标解读¶
| 指标 | 含义 | 说明 |
|---|---|---|
used_memory | Redis 实际使用的内存 | 包含所有数据、元数据 |
used_memory_rss | OS 分配给 Redis 的物理内存 | 包含内存碎片 |
mem_fragmentation_ratio | 内存碎片率 = rss / used | 正常值 1.0~1.5 |
used_memory_peak | 历史内存使用峰值 | 用于评估内存水位 |
used_memory_lua | Lua 引擎占用内存 | Lua 脚本过多时需关注 |
内存碎片率判断:
mem_fragmentation_ratio < 1.0 → 内存不足,Redis 在使用 Swap(严重!)
mem_fragmentation_ratio 1.0~1.5 → 正常
mem_fragmentation_ratio > 1.5 → 碎片率过高,需要处理
2.3 内存碎片整理¶
Redis 4.0+ 支持在线内存碎片整理(无需重启):
# 查看碎片率
redis-cli INFO memory | grep mem_fragmentation_ratio
# 开启自动碎片整理(推荐)
redis-cli CONFIG SET activedefrag yes
# 碎片整理触发阈值配置
redis-cli CONFIG SET active-defrag-ignore-bytes 100mb # 碎片超过100MB才整理
redis-cli CONFIG SET active-defrag-enabled yes
redis-cli CONFIG SET active-defrag-threshold-lower 10 # 碎片率超过10%触发
redis-cli CONFIG SET active-defrag-threshold-upper 100 # 碎片率超过100%全力整理
⚠️ 碎片整理会消耗 CPU,建议在低峰期开启,或设置合理的 CPU 使用上限(
active-defrag-max-scan-fields)。
3. Key 过期删除机制¶
Redis 有两种过期 Key 删除策略,同时使用,互补不足:
📖 术语家族:过期删除策略族(Expiration Strategies)
字面义:Key 设置了 TTL 到期后,谁来负责把它从内存里抹掉的一组策略家族。 在 Redis 中的含义:Redis 没有选「为每个 Key 起一个定时器」的方案(会吃掉大量 CPU、破坏单线程模型),而是用「懒一点 + 定期扫一点」的组合——两种都漏网的残余 Key,最终由 §4 的淘汰策略兜底。 同家族成员:
| 成员 | 触发时机 | 扫描方式 | 源码入口 | 优缺点 |
|---|---|---|---|---|
| 惰性删除(Lazy) | 下次 GET/SET 访问该 Key 时 | 单 Key 点查 | db.c: expireIfNeeded() | ✅ 零主动 CPU ❌ 不访问就永远不删,内存泄漏风险 |
| 定期删除(Active) | 每 100ms(hz=10) | 从 expires 字典随机抽 20 个 | expire.c: activeExpireCycle() | ✅ 主动回收 ❌ 随机抽,不保证全扫到 |
| ~~定时删除~~(Timer) | Key 一到期立刻触发 | 每 Key 一个定时器 | Redis 未采用 | ✅ 最及时 ❌ 单线程下 CPU 开销爆炸 |
命名规律:懒(Lazy)= 访问触发、勤(Active)= 轮询触发、准(Timer)= 定时器触发——Redis 选了「懒 + 勤」这对组合拳,用随机采样把全表扫的开销均摊到 100ms 的小步中。
3.1 惰性删除(Lazy Expiration)¶
原理:Key 过期后不立即删除,等到下次访问该 Key 时才检查是否过期,过期则删除并返回 nil。
flowchart LR
A[GET key] --> B{Key 是否过期?}
B -->|未过期| C[返回 value]
B -->|已过期| D[删除 Key]
D --> E[返回 nil] 优点:节省 CPU,不主动扫描
缺点:过期 Key 如果一直不被访问,会一直占用内存(内存泄漏风险)
3.2 定期删除(Active Expiration)¶
原理:Redis 每隔 100ms 随机抽取一批设置了过期时间的 Key,检查并删除已过期的 Key。
执行流程:
每 100ms 执行一次:
1. 从 expires 字典中随机抽取 20 个 Key
2. 删除其中已过期的 Key
3. 如果本批次过期 Key 比例 > 25%,立即再执行一次(直到比例 < 25% 或超时)
优点:主动回收内存,避免大量过期 Key 堆积
缺点:随机抽取,不能保证所有过期 Key 都被及时清理
3.3 两种策略对比¶
| 策略 | 触发时机 | CPU 消耗 | 内存回收 |
|---|---|---|---|
| 惰性删除 | 访问时 | 低 | 不及时 |
| 定期删除 | 每 100ms | 中 | 较及时 |
两者结合:定期删除兜底大部分过期 Key,惰性删除处理漏网之鱼。但如果过期 Key 既不被访问、又没被定期删除抽到,就需要内存淘汰策略来兜底。
4. 内存淘汰策略(8 种)¶
当 Redis 内存达到 maxmemory 上限时,触发淘汰策略,决定淘汰哪些 Key 来腾出空间。
📖 术语家族:淘汰策略族(Eviction Policies)
字面义:maxmemory-policy 配置项的 8 个可选值,决定「内存满了之后,从谁身上挤出空间」。 在 Redis 中的含义:8 种策略是 2 × 4 的正交组合——「淘汰范围(针对哪批 Key)」× 「淘汰算法(按什么指标排序)」,再加一个「完全不淘汰」的兜底项。只要记住这两个维度,8 种策略不用背。
维度①:淘汰范围前缀
| 前缀 | 含义 | 典型场景 |
|---|---|---|
allkeys-* | 所有 Key 都参与淘汰 | 纯缓存(所有 Key 都是缓存数据,随便淘) |
volatile-* | 只淘汰设置了 TTL 的 Key | 缓存 + 持久化混用(保护无 TTL 的业务数据) |
noeviction | 不淘汰,写入直接报 OOM | 不允许丢数据(Redis 作为 KV 存储而非缓存) |
维度②:淘汰算法后缀
| 后缀 | 淘汰依据 | 特点 | Redis 实现 |
|---|---|---|---|
-lru | 最近最少使用(时间维度) | 近似 LRU,采样 N 个选最旧 | 每 Key 24 bit lru_clock(精度 10 秒) |
-lfu | 最少频率使用(次数维度) | 对数计数器 + 时间衰减 | 每 Key 8 bit 计数器 + 16 bit 衰减时间戳(Redis 4.0+) |
-random | 随机挑一个 | 无脑快 | — |
-ttl | 剩余 TTL 最短 | 仅 volatile-ttl 一种组合 | — |
同家族成员完整矩阵(8 = 1 + 3 + 4):
不淘汰: noeviction
全 Key: allkeys-lru | allkeys-lfu | allkeys-random
有 TTL: volatile-lru | volatile-lfu | volatile-random | volatile-ttl
命名规律:<范围>-<算法> = 「在 <范围> 内按 <算法> 淘汰」——先看前缀定谁有资格被淘汰,再看后缀定淘汰谁,8 选 1 就是这么朴素。
4.1 配置 maxmemory¶
# redis.conf 配置
maxmemory 4gb # 最大内存限制
maxmemory-policy allkeys-lru # 淘汰策略
# 动态修改(无需重启)
redis-cli CONFIG SET maxmemory 4gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
4.2 8 种淘汰策略详解¶
mindmap
root((淘汰策略))
不淘汰
noeviction
针对所有 Key
allkeys-lru
allkeys-lfu
allkeys-random
针对有过期时间的 Key
volatile-lru
volatile-lfu
volatile-random
volatile-ttl | 策略 | 淘汰范围 | 淘汰规则 | 适用场景 |
|---|---|---|---|
noeviction | — | 不淘汰,内存满时写入报错 | 不允许数据丢失的场景(默认值) |
allkeys-lru | 所有 Key | 淘汰最近最少使用的 Key | 最常用,纯缓存场景 |
allkeys-lfu | 所有 Key | 淘汰访问频率最低的 Key | 热点数据明显的场景(Redis 4.0+) |
allkeys-random | 所有 Key | 随机淘汰 | 数据访问均匀,无明显热点 |
volatile-lru | 有过期时间的 Key | 淘汰最近最少使用的 Key | 缓存+持久化混用场景 |
volatile-lfu | 有过期时间的 Key | 淘汰访问频率最低的 Key | 缓存+持久化混用场景(Redis 4.0+) |
volatile-random | 有过期时间的 Key | 随机淘汰 | 较少使用 |
volatile-ttl | 有过期时间的 Key | 淘汰剩余 TTL 最短的 Key | 希望优先淘汰即将过期的数据 |
4.3 如何选择淘汰策略?¶
flowchart TD
A[Redis 用途是什么?] --> B{纯缓存?}
B -->|是| C{热点数据明显?}
C -->|是| D[allkeys-lfu<br>保留热点数据]
C -->|否| E[allkeys-lru<br>最通用]
B -->|否| F{缓存+持久化混用?}
F -->|是| G[volatile-lru<br>只淘汰有过期时间的缓存Key]
F -->|否| H[noeviction<br>不允许丢失数据] 生产推荐:
- 纯缓存:
allkeys-lru(最常用,简单有效) - 热点数据明显:
allkeys-lfu(LFU 比 LRU 更能保留真正的热点) - 缓存与持久化数据混用:
volatile-lru(只淘汰设置了过期时间的缓存 Key,保护持久化数据)
5. LRU 与 LFU 算法原理¶
5.1 LRU(Least Recently Used,最近最少使用)¶
标准 LRU:维护一个双向链表,每次访问将 Key 移到链表头部,淘汰时删除链表尾部的 Key。
Redis 的近似 LRU:Redis 并未实现标准 LRU(维护链表开销大),而是使用近似 LRU:
近似 LRU 的问题:无法区分"偶尔访问一次的 Key"和"频繁访问的热点 Key",可能淘汰真正的热点。
5.2 LFU(Least Frequently Used,最少频率使用)¶
Redis 4.0 引入,每个 Key 维护一个访问频率计数器,淘汰访问频率最低的 Key。
Redis LFU 的特殊设计:
LRU vs LFU 对比:
| 对比项 | LRU | LFU |
|---|---|---|
| 淘汰依据 | 最近访问时间 | 访问频率 |
| 热点保护 | 一般(偶发访问也能保留) | 好(真正高频的才保留) |
| 冷启动 | 好(新 Key 不会立即被淘汰) | 差(新 Key 频率为0,容易被淘汰) |
| 适用场景 | 通用 | 热点数据明显的场景 |
6. 内存优化实践¶
6.1 合理设置 maxmemory¶
# 建议:maxmemory 设置为物理内存的 60%~80%
# 预留空间给:AOF rewrite、主从复制缓冲区、Lua 脚本等
# 查看当前内存使用
redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human"
6.2 使用合适的数据结构节省内存¶
// ❌ 用 String 存对象(每个 Key 都有元数据开销)
redis.set("user:1001:name", "张三");
redis.set("user:1001:age", "25");
redis.set("user:1001:email", "[email protected]");
// 3个 Key,每个 Key 约 50~100 字节元数据开销
// ✅ 用 Hash 存对象(共享 Key 元数据)
redis.hset("user:1001", "name", "张三", "age", "25", "email", "[email protected]");
// 1个 Key,元素数 < 128 时使用 ziplist,内存节省 60%+
6.3 控制 Key 的数量和大小¶
# 查看 Key 数量
redis-cli DBSIZE
# 查看内存使用分布(按前缀统计)
redis-cli --scan --pattern "user:*" | wc -l
# 分析内存占用(采样分析,不阻塞)
redis-cli --memkeys
6.4 设置合理的 TTL¶
// ❌ 不设置过期时间(内存只增不减)
redis.set("cache:product:1001", productJson);
// ✅ 设置过期时间 + 随机偏移(避免雪崩)
int ttl = 3600 + ThreadLocalRandom.current().nextInt(600); // 3600~4200秒
redis.setex("cache:product:1001", ttl, productJson);
7. 常见问题¶
Q:Redis 内存碎片率高怎么处理?
- 开启自动碎片整理(Redis 4.0+):
CONFIG SET activedefrag yes,Redis 会在后台自动整理碎片,对业务无感知- 重启 Redis:重启后内存重新分配,碎片消失(需要配合持久化,避免数据丢失)
- 主从切换:先让从节点重启整理碎片,再切换主从角色
Q:noeviction 策略下内存满了会怎样?
所有写命令(SET/LPUSH/ZADD 等)都会返回错误
OOM command not allowed when used memory > 'maxmemory',只读命令(GET/LRANGE 等)仍然可以执行。
Q:LRU 和 LFU 怎么选?
- 大多数场景用
allkeys-lru,简单可靠- 如果业务有明显的热点数据(如秒杀商品、热搜词),用
allkeys-lfu,能更好地保留真正的热点 Key- 新上线的系统建议先用 LRU,稳定后根据监控数据决定是否切换 LFU
Q:volatile-lru 和 allkeys-lru 怎么选?
- 如果 Redis 中只存缓存数据(所有 Key 都有 TTL):用
allkeys-lru,淘汰范围更大,效果更好- 如果 Redis 中同时存缓存和持久化数据(部分 Key 无 TTL):用
volatile-lru,只淘汰有过期时间的缓存 Key,保护无 TTL 的持久化数据
Q:Redis 过期 Key 删除为什么不用定时删除?
定时删除(为每个 Key 创建定时器,到期立即删除)虽然内存回收最及时,但 Redis 是单线程模型,大量定时器会占用大量 CPU,影响正常命令处理。因此 Redis 选择惰性删除 + 定期删除的组合,在 CPU 和内存之间取得平衡。
复习检验标准:能否说出 8 种淘汰策略并知道各自适用场景?能否解释 Redis 的过期 Key 删除机制(惰性+定期)?能否解释内存碎片率的含义及处理方式?能否说出 LRU 和 LFU 的区别?