本文综合 Redis 内存分配策略、过期键删除机制、内存淘汰策略、采样机制、LFU 原理及异步删除配置,带你彻底搞懂 Redis 内存管理的方方面面。
一、内存上限配置:maxmemory
Redis 默认不限制内存使用(64 位系统),生产环境必须主动设置上限,防止 Redis 耗尽服务器内存导致 OOM。
在 Redis 的配置文件 redis.conf 中,配置 maxmemory 的大小参数如下所示:
bash
# redis.conf 配置
maxmemory 4gb
# 运行时动态修改
CONFIG SET maxmemory 4gb
CONFIG GET maxmemory
建议设为物理内存的 3/4 左右,留出空间给操作系统和其他进程。一般小公司设置为 3G 左右,实际生产肯定不是 100mb。
二、过期键删除策略
Redis 中处理过期键有三种理论策略,它们在 CPU 和内存之间做出了不同的权衡:
| 策略 | 执行方式 | 对 CPU 的影响 | 对内存的影响 | Redis 是否采用 |
|---|---|---|---|---|
| 定时删除 | 为每个设置了过期时间的 key 创建一个定时器,时间一到立即删除 | ❌ 高,维护大量定时器消耗 CPU | ✅ 最友好,过期键第一时间释放 | ❌ 未采用 |
| 惰性删除 | 每次访问 key 时检查是否过期,若过期则删除 | ✅ 低,仅在访问时检查 | ❌ 差,过期但未访问的 key 会一直占用内存 | ✅ 采用(作为兜底) |
| 定期删除 | 每隔一段时间(默认 100ms)随机抽取一批设置了过期时间的 key,删除其中已过期的 | ✅ 可控,通过限制执行时间和扫描数量控制开销 | ✅ 较好,能及时清理大部分过期键 | ✅ 采用(主力) |
2.1 定时删除
- 为每个 key 设置一个定时器,到期立即执行删除。
- 优点:内存释放最及时,不存在内存浪费。
- 缺点:CPU 负担极重。如果有大量 key 设置了不同的过期时间,Redis 需要维护大量定时器,这在高并发场景下是不可接受的。
- 为什么 Redis 不用:Redis 是单线程模型,定时器会抢占主线程的执行时间,严重影响服务性能。
2.2 惰性删除
- 只有当 key 被访问(读/写)时,才检查其是否过期,若过期则删除。
- 优点:CPU 开销极低,无需任何额外维护。
- 缺点:如果某些过期 key 永远不会再被访问,它们会一直占用内存,造成"内存泄漏"。
- Redis 中的角色:作为兜底机制,配合定期删除使用。定期删除漏掉的那些过期 key,最终在访问时会被惰性删除清理。
2.3 定期删除
- Redis 内部有一个定时任务(
serverCron),默认每秒运行 10 次(可通过hz配置调整)。每次执行时,会随机抽取一批设置了过期时间的 key,删除其中已过期的。 - 优点:折中了 CPU 和内存。通过限制每次执行的时间和扫描数量,将 CPU 开销控制在一定范围内,同时又能及时清理大部分过期键。
- 缺点:无法保证所有过期键都在第一时间被删除(会有少量残留)。
- Redis 中的角色:主力过期清理机制。
2.4 Redis 实际采用的方案:定期删除 + 惰性删除
- 定期删除负责主动批量清理,控制内存占用。
- 惰性删除负责兜底,确保即使定期删除漏掉了一些过期键,在访问时也会被清理。
这种组合方案在不牺牲 CPU 性能 的前提下,最大程度地保证了内存不会被过期键无限占用,是 Redis 高性能设计中的一个经典权衡。
三、内存淘汰策略
当实际的存储中超出 Redis 的 maxmemory 配置大小时,Redis 中有淘汰策略 ,把需要淘汰的 key 给淘汰掉,整理出干净的一块内存给新的 key 值使用。
Redis 6.0+ 共提供 8 种淘汰策略,可按其核心逻辑分为 四大类:
| 类别 | 策略名 | 核心逻辑 | 一句话总结 |
|---|---|---|---|
| 🚫 不淘汰 | noeviction |
内存满了就拒绝新写入,直接报错 | 硬拒绝,宁可报错也不删数据 |
| 🎲 随机淘汰 | allkeys-random volatile-random |
随机挑选 Key 淘汰 | 全凭运气,谁被选中谁淘汰 |
| ⏰ 基于时间的淘汰 | volatile-ttl |
淘汰剩余存活时间(TTL)最短的 Key | 赶早不赶晚,优先清除快要过期的数据 |
| 📈 基于频率/时间的淘汰 | allkeys-lru volatile-lru allkeys-lfu volatile-lfu |
淘汰最久未使用(LRU)或最不经常使用(LFU)的 Key | 优胜劣汰,留下访问最频繁/最新的核心数据 |
volatile-xxx 系列 :只淘汰设置了过期时间的 Key,未设置过期时间的 Key 不会被淘汰,适合需要持久保留重要数据的场景。
3.1 noeviction ------ 宁死不屈
- 行为 :内存超限后,所有写入命令(
SET、LPUSH、SADD等)返回错误,读命令正常。 - 是否采样:❌ 不需要,直接拒绝写入。
- 场景 :金融交易、绝对不允许数据丢失的系统。生产环境很少使用,因为一旦写满,业务会直接失败。
3.2 随机淘汰 ------ 纯运气
- allkeys-random:从所有 Key 中随机淘汰。
- volatile-random:仅从设置了过期时间的 Key 中随机淘汰。
- 是否采样 :❌ 不需要,直接调用
dictGetRandomKey()随机抽取。 - 实现:复杂度 O(1),无额外计算。
- 场景:数据访问概率均匀,没有明显热点。例如存放临时日志的缓存。
3.3 volatile-ttl ------ 谁快过期谁先走
- 行为:只从设置了过期时间的 Key 中,挑选剩余 TTL 最短的淘汰。
- 是否采样:✅ 需要采样。默认随机采样 5 个 Key,取其中 TTL 最小的淘汰。
- 实现 :采样个数由
maxmemory-samples配置(默认 5)。 - 场景:优惠券、验证码、临时会话等"短命数据",即将过期的数据价值最低。
3.4 LRU 系列 ------ 最近最少使用
- 行为 :淘汰 最后一次访问时间 最久远的 Key。
- 是否采样 :✅ 需要采样。默认随机采样 5 个 Key,淘汰其中
lru时间最小的 Key。 - 实现 :近似 LRU,每个
redisObject有 24 位lru字段记录最后访问时间戳(秒级精度)。采样数量越大,越接近真实 LRU,但 CPU 开销也越大。 - 场景:通用缓存,符合"二八原则"(20% 数据承载 80% 请求),例如热门新闻、商品信息。
- 缺点:容易被"一次性查询"冲垮热点数据(例如批量导出报表)。
3.5 LFU 系列 ------ 最不经常使用(4.0+)
- 行为 :淘汰 访问频率 最低的 Key,频率会随时间衰减。
- 是否采样 :✅ 需要采样。默认随机采样 5 个 Key,淘汰其中
logc最小的 Key(经过衰减计算)。 - 实现 :复用 24 位
lru字段,拆分为 16 位 LDT(上次衰减时间) + 8 位 LOGCNT(对数计数器)。 - 场景:长期稳定的热点数据,如爆款商品、热门文章。避免了 LRU 被突发冷访问挤掉热点的缺陷。
四、LFU 深度解析
4.1 对数计数器 LOGCNT
8 位只能存 0~255,如果用线性计数(每次访问 +1),访问 256 次后就饱和了,无法区分 1000 次/秒 和 10000 次/秒 的热度。因此 Redis 使用 对数增长:访问次数越高,计数器增加的概率越低。
每次访问时,以概率 p 增加 logc:
p = 1 / (logc * lfu_log_factor + 1)
其中 lfu_log_factor 是可配置参数,默认 10。
例子(lfu_log_factor = 10):
| 实际访问次数 | 对数计数器 logc | 说明 |
|---|---|---|
| 0 | 0 | 初始 |
| 10 | 1 | 前 10 次访问快速上升到 1 |
| 100 | ~3 | 100 次访问后约 3 |
| 1000 | ~6 | 1000 次访问后约 6 |
| 1,000,000 | ~10 | 百万次访问后约 10 |
这样,即使实际访问量相差巨大,logc 也能在 0~255 内合理区分热度。
4.2 衰减机制 LDT
如果一个 Key 长时间没被访问,它的热度应该自然下降。Redis 通过 ldt 记录上次衰减的时间(单位:分钟),每次访问时计算时间差,减去相应的计数。
配置参数 lfu-decay-time(默认 1)表示每隔多少分钟衰减 1。
例子:
- 当前
logc = 100,ldt = 1000(分钟时间戳) - 1 分钟后(当前时间 = 1001 分钟):
delta = 1→ 衰减 1 次 →logc = 99,更新ldt = 1001 - 10 分钟后(当前时间 = 1010 分钟):
delta = 10→ 衰减 10 次 →logc = 90,更新ldt = 1010
如果没有衰减,热点 Key 会永远占据内存,不合理。
五、淘汰策略何时触发?
触发时机:执行写命令时(同步检查)
- 客户端发送写命令(如
SET、HSET等)。 - Redis 检查当前已用内存是否 ≥
maxmemory。- 若未超过 → 正常执行命令。
- 若已超过 → 进入淘汰流程。
- 循环执行淘汰(一次可能淘汰多个 Key),直到内存降至
maxmemory以下,或无法再淘汰(如所有 Key 都被尝试过)。 - 若淘汰成功,则执行原写命令;若淘汰后内存仍不足且策略为
noeviction,则返回错误。
注意:读操作不会触发淘汰 ,也没有专门的定时线程去跑淘汰(只有过期删除有定时任务)。淘汰是被动附着在写命令上的。
六、异步删除机制:DEL vs UNLINK
6.1 历史问题
早期 Redis(<4.0)淘汰时统一使用 同步 DEL 。如果淘汰的是大 Key(如包含数百万元素的 Hash),DEL 会在主线程中遍历释放内存,造成 长时间阻塞,服务不可用。
6.2 异步删除命令 UNLINK(4.0 引入)
UNLINK 将删除工作拆分为两步:
- 主线程:从全局字典中摘除该 Key(O(1) 极快)。
- 后台线程:异步回收实际 value 占用的内存。
对于大 Key,UNLINK 几乎不阻塞主线程。
6.3 淘汰策略的删除方式可配置
Redis 提供了配置项 lazyfree-lazy-eviction,用于控制内存淘汰时使用同步还是异步删除:
bash
# redis.conf
lazyfree-lazy-eviction yes # 淘汰时使用 UNLINK(异步)
lazyfree-lazy-eviction no # 淘汰时使用 DEL(同步,默认)
生产环境强烈建议设置为
yes,避免因淘汰大 Key 引发服务抖动。
6.4 其他异步删除场景
Redis 4.0+ 还提供了类似的配置控制:
| 配置项 | 作用 | 默认值 |
|---|---|---|
lazyfree-lazy-eviction |
淘汰策略淘汰时 | no |
lazyfree-lazy-expire |
过期 Key 删除时 | no |
lazyfree-lazy-server-del |
命令内部替换 Key 时(如 RENAME) |
no |
replica-lazy-flush |
主从全量同步清空从库时 | no |
建议生产环境将前两项开启。
七、采样机制详解
哪些策略需要采样?
| 策略类型 | 是否需要采样 | 原因 |
|---|---|---|
noeviction |
❌ 不需要 | 直接拒绝写入,不涉及淘汰 |
allkeys-random / volatile-random |
❌ 不需要 | 随机淘汰,直接调用 dictGetRandomKey() |
volatile-ttl |
✅ 需要 | 需要从过期 Key 中挑出 TTL 最小的 |
lru 系列 |
✅ 需要 | 需要从候选集中挑出最久未使用的 |
lfu 系列 |
✅ 需要 | 需要从候选集中挑出访问频率最低的 |
采样参数配置:
bash
# 默认采样 5 个 Key,范围 1~64
maxmemory-samples 5
采样数越大,淘汰越精确,但 CPU 开销也越大。一般保持默认 5 即可,对性能影响极小。
八、如何选择淘汰策略?
| 需求 | 推荐策略 |
|---|---|
| 通用缓存,不知道选什么 | allkeys-lru |
| 热点数据稳定,访问模式长期不变 | allkeys-lfu |
| 需要区分持久数据(无过期)和临时数据 | volatile-lru 或 volatile-lfu |
| 数据访问概率均匀 | allkeys-random |
| 临时数据优先淘汰快过期的 | volatile-ttl |
| 绝对不允许丢数据(慎用) | noeviction |
九、最佳实践总结
- 必须设置
maxmemory,并选择合适的淘汰策略。 - 推荐策略 :
allkeys-lru或allkeys-lfu(如果 Redis 版本 ≥4.0 且业务热点稳定)。 - 开启异步删除 :
lazyfree-lazy-eviction yes,防止淘汰大 Key 阻塞。 - 监控内存使用率,设置告警阈值(如 80%),及时扩容或优化数据。
- 如果业务有强持久化要求,可以考虑 主从 + 哨兵 ,将淘汰策略设为主库淘汰,从库不淘汰(通过配置
slave-read-only)。 - 避免使用
noeviction作为线上策略,除非你能接受写失败。
十、常见面试题速答
Q: Redis 内存淘汰是在读请求还是写请求触发?
A: 写请求触发,读请求不触发。
Q: 淘汰策略是实时扫描全部 Key 吗?
A: 不是。LRU/LFU/TTL 都是采样淘汰(默认 5 个),性能可控。
Q: 哪些策略不需要采样?
A: noeviction 和所有 random 系列(allkeys-random、volatile-random)。
Q: 淘汰大 Key 会阻塞 Redis 吗?
A: 默认会用 DEL 同步删除,会阻塞。应开启 lazyfree-lazy-eviction 让其异步。
Q: volatile-lru 和 allkeys-lru 区别?
A: 前者只淘汰设置了过期时间的 Key,后者淘汰所有 Key。
Q: 如何修改淘汰策略?
A: CONFIG SET maxmemory-policy allkeys-lru
Q: LFU 如何解决 LRU 被一次性查询冲垮热点的问题?
A: LFU 记录访问频率而非最后时间,且频率会随时间衰减。一次性查询虽然"最近"被访问,但频率很低,不会被误判为热点。
Q: Redis 过期键删除和内存淘汰有什么区别?
A: 过期键删除针对的是设置了过期时间且已到期 的 Key,由定期删除+惰性删除处理;内存淘汰针对的是内存达到 maxmemory 上限时的主动清理,由淘汰策略处理。两者触发条件和机制不同。
希望这篇文章能帮你彻底理清 Redis 内存管理的方方面面,在生产环境中游刃有余地配置和管理 Redis 内存。