Redis 内存管理深度解析:过期删除与内存淘汰策略

本文综合 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 ------ 宁死不屈

  • 行为 :内存超限后,所有写入命令(SETLPUSHSADD 等)返回错误,读命令正常。
  • 是否采样:❌ 不需要,直接拒绝写入。
  • 场景 :金融交易、绝对不允许数据丢失的系统。生产环境很少使用,因为一旦写满,业务会直接失败。

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 = 100ldt = 1000(分钟时间戳)
  • 1 分钟后(当前时间 = 1001 分钟):delta = 1 → 衰减 1 次 → logc = 99,更新 ldt = 1001
  • 10 分钟后(当前时间 = 1010 分钟):delta = 10 → 衰减 10 次 → logc = 90,更新 ldt = 1010

如果没有衰减,热点 Key 会永远占据内存,不合理。


五、淘汰策略何时触发?

触发时机:执行写命令时(同步检查)

  1. 客户端发送写命令(如 SETHSET 等)。
  2. Redis 检查当前已用内存是否 ≥ maxmemory
    • 若未超过 → 正常执行命令。
    • 若已超过 → 进入淘汰流程。
  3. 循环执行淘汰(一次可能淘汰多个 Key),直到内存降至 maxmemory 以下,或无法再淘汰(如所有 Key 都被尝试过)。
  4. 若淘汰成功,则执行原写命令;若淘汰后内存仍不足且策略为 noeviction,则返回错误。

注意:读操作不会触发淘汰 ,也没有专门的定时线程去跑淘汰(只有过期删除有定时任务)。淘汰是被动附着在写命令上的。


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-lruvolatile-lfu
数据访问概率均匀 allkeys-random
临时数据优先淘汰快过期的 volatile-ttl
绝对不允许丢数据(慎用) noeviction

九、最佳实践总结

  1. 必须设置 maxmemory,并选择合适的淘汰策略。
  2. 推荐策略allkeys-lruallkeys-lfu(如果 Redis 版本 ≥4.0 且业务热点稳定)。
  3. 开启异步删除lazyfree-lazy-eviction yes,防止淘汰大 Key 阻塞。
  4. 监控内存使用率,设置告警阈值(如 80%),及时扩容或优化数据。
  5. 如果业务有强持久化要求,可以考虑 主从 + 哨兵 ,将淘汰策略设为主库淘汰,从库不淘汰(通过配置 slave-read-only)。
  6. 避免使用 noeviction 作为线上策略,除非你能接受写失败。

十、常见面试题速答

Q: Redis 内存淘汰是在读请求还是写请求触发?

A: 写请求触发,读请求不触发。

Q: 淘汰策略是实时扫描全部 Key 吗?

A: 不是。LRU/LFU/TTL 都是采样淘汰(默认 5 个),性能可控。

Q: 哪些策略不需要采样?

A: noeviction 和所有 random 系列(allkeys-randomvolatile-random)。

Q: 淘汰大 Key 会阻塞 Redis 吗?

A: 默认会用 DEL 同步删除,会阻塞。应开启 lazyfree-lazy-eviction 让其异步。

Q: volatile-lruallkeys-lru 区别?

A: 前者只淘汰设置了过期时间的 Key,后者淘汰所有 Key。

Q: 如何修改淘汰策略?

A: CONFIG SET maxmemory-policy allkeys-lru

Q: LFU 如何解决 LRU 被一次性查询冲垮热点的问题?

A: LFU 记录访问频率而非最后时间,且频率会随时间衰减。一次性查询虽然"最近"被访问,但频率很低,不会被误判为热点。

Q: Redis 过期键删除和内存淘汰有什么区别?

A: 过期键删除针对的是设置了过期时间且已到期 的 Key,由定期删除+惰性删除处理;内存淘汰针对的是内存达到 maxmemory 上限时的主动清理,由淘汰策略处理。两者触发条件和机制不同。


希望这篇文章能帮你彻底理清 Redis 内存管理的方方面面,在生产环境中游刃有余地配置和管理 Redis 内存。

相关推荐
Solis程序员3 小时前
分层缓存调度:削峰控压下的 Feed 流高性能设计
缓存
九皇叔叔3 小时前
高斯性能分析【第一天】单表执行计划分析
java·数据库·性能分析·执行计划·gauss
Mr. zhihao3 小时前
Redis 持久化完全指南:从 RDB、AOF 到 MP-AOF
redis
難釋懷3 小时前
Redis内存回收-过期key处理
数据库·redis·缓存
KaMeidebaby3 小时前
卡梅德生物技术快报|PROTAC 药物降解蛋白原理及数据库平台开发全流程
前端·数据库·其他·百度·新浪微博
鱼鳞_3 小时前
苍穹外卖-Day05(Redis)
java·redis
是码龙不是码农3 小时前
数据库主键选型:为什么别用自增 ID?
java·数据库
IpdataCloud3 小时前
企业IT管理中,如何通过IP地址查询定位快速溯源异常终端?用IP离线库实现
服务器·网络·数据库·tcp/ip
罗超驿4 小时前
20.MySQL事务隔离级别示例详解(脏读、不可重复读、幻读)
java·数据库·mysql·面试