Redis过期策略与内存淘汰机制解析
一、Redis 过期策略详解:如何处理"过期的键"?
Redis 作为高性能键值数据库,支持为键设置过期时间(通过EXPIRE、PEXPIRE等命令)。当键的过期时间到达后,Redis 需要将其从内存中清理,避免无效数据占用资源。但如果直接"实时清理"所有过期键,会严重影响 Redis 性能(比如大量键同时过期时,CPU 会被频繁占用)。因此,Redis 采用了"惰性删除 + 定期删除"的混合策略,在"性能"和"内存占用"之间取得平衡。
1.1 惰性删除(Lazy Expiration):"用的时候再检查"
核心原理
惰性删除的核心逻辑是:不主动监测键是否过期,只有当某个键被访问时(如GET、SETNX等命令),才会检查它是否过期。如果过期,则删除该键并返回nil;如果未过期,则正常返回键值。
执行流程
- 客户端发起访问某个键的命令(如GET key);
- Redis 先检查该键是否存在过期时间(expires字典中是否有记录);
- 若不存在过期时间,直接返回键值;
- 若存在过期时间,对比当前时间与过期时间:
- 若已过期:删除该键(从dict字典和expires字典中移除),返回nil;
- 若未过期:正常返回键值。
实际应用场景
- 热点数据访问:经常被访问的键会及时被发现并清理
- 低频访问数据:如后台日志缓存键可能长期不被发现
优缺点分析
优点
- 性能优先:无需主动扫描过期键,CPU 资源消耗极低,不影响 Redis 正常读写
- 实现简单:逻辑清晰,无需复杂的定时任务调度
- 精准删除:只删除被访问的过期键,不会误删有效键
缺点
- 内存泄漏风险:如果过期键长期不被访问,会一直占用内存,导致"无效内存膨胀"
- 可能触发"批量删除":若大量过期键突然被访问(如定时任务批量查询),会集中删除键,短暂阻塞线程
- 无法保证及时清理:冷数据可能长期驻留内存
1.2 定期删除(Periodic Expiration):"定时批量检查"
为解决惰性删除的"内存泄漏"问题,Redis 引入了定期删除策略:每隔一段时间,主动扫描部分过期键并删除。但为了避免扫描过程占用过多 CPU,Redis 做了"有限度"的扫描 —— 不扫描所有键,只扫描部分过期键。
核心原理
扫描频率:
- 由 Redis 配置hz控制(默认hz=10,即每秒扫描 10 次)
- hz值越大,扫描频率越高,过期键清理越及时,但 CPU 占用也越高
- 建议生产环境不超过hz=50,需要根据实际负载调整
扫描范围:
- 每次扫描时,Redis 会:
- 从expires字典(存储所有带过期时间的键)中随机抽取N个键(N默认是 20)
- 检查这N个键是否过期,若过期则删除
- 若删除的键数占比超过25%(即删除了超过 5 个键),则继续重复上述步骤(再抽 20 个键检查)
- 若占比≤25%,则停止本次扫描,等待下一次hz触发
为什么要"停止扫描"?
这是 Redis 的"保护机制":如果某一时刻有大量过期键(如缓存雪崩场景),若无限循环扫描,会导致 Redis 线程一直阻塞在"删除过期键"的逻辑中,无法处理客户端请求。通过"删除占比≤25% 则停止"的规则,既能清理部分过期键,又能保证 Redis 的正常服务。
配置建议
# redis.conf配置示例
hz 10 # 默认值,每秒扫描10次
maxmemory-policy volatile-lru # 配合内存淘汰策略
优缺点分析
优点
- 平衡内存与性能:定期清理过期键,减少无效内存占用,同时通过"有限扫描"控制 CPU 消耗
- 避免批量阻塞:通过"25% 占比停止"规则,防止扫描逻辑长时间阻塞线程
- 渐进式清理:分批次处理过期键,避免一次性大负载
缺点
- 仍有漏网之鱼:无法保证所有过期键都被及时清理(比如未被扫描到的过期键,仍需等待惰性删除)
- 扫描频率难把控:hz值设置过小,清理不及时;设置过大,CPU 占用过高
- 随机性影响效果:随机抽样的方式可能导致某些键长期不被检查
1.3 两种策略的协同工作流程
惰性删除和定期删除并非独立存在,而是协同工作,形成 Redis 过期键处理的完整逻辑:
- 日常读写:依赖惰性删除,确保访问时只返回有效数据,同时避免主动 CPU 消耗
- 后台清理:依赖定期删除,每隔一段时间批量清理部分过期键,缓解内存膨胀问题
- 极端场景:若定期删除未清理的过期键,且长期不被访问,仍会占用内存 —— 这时候需要依赖"内存淘汰机制"进一步处理
典型工作流程示例
- 键A设置10秒过期
- 5秒后被访问:惰性删除检查未过期,正常返回
- 12秒时定期删除扫描到键A并删除
- 若键A未被定期扫描到,15秒时被访问:惰性删除发现已过期,立即删除
- 若键A既未被扫描也未被访问,最终会被内存淘汰机制处理
二、Redis 内存淘汰机制:内存不足时该删谁?
当 Redis 的内存使用达到配置的 maxmemory(最大内存限制)时,再写入新键会触发内存淘汰机制:Redis 会根据预设的规则,删除部分数据,腾出内存空间供新键使用。这个机制是 Redis 作为内存数据库的关键特性之一,它确保了 Redis 在内存有限的情况下仍能继续工作。
2.1 前置条件:开启内存限制
内存限制的重要性
首先需要明确:内存淘汰机制仅在 maxmemory 配置生效时触发。若未设置 maxmemory(默认无限制),Redis 会一直使用内存,直到操作系统触发内存溢出(OOM),导致 Redis 进程被杀死。这种情况会导致服务突然中断,可能造成严重的数据丢失和服务不可用。
生产环境配置建议
生产环境必须配置 maxmemory,建议设置为物理内存的 70%-80%(例如 8GB 内存的机器,maxmemory=6GB),避免 Redis 占用过多内存影响其他进程。这个比例考虑了:
- 操作系统正常运行所需的内存
- Redis 子进程(如 RDB 持久化或 AOF 重写)需要的额外内存
- 其他系统进程的内存需求
配置方式
在 redis.conf 文件中配置:
# 单位:字节(也可加单位,如1gb、512mb)
maxmemory 6442450944 # 6GB的示例
或者在运行时通过命令配置:
CONFIG SET maxmemory 6gb
2.2 8 种内存淘汰策略:从 "不淘汰" 到 "智能淘汰"
Redis 4.0+ 共支持 8 种内存淘汰策略,可通过 maxmemory-policy 配置(默认 noeviction)。根据淘汰逻辑,可分为 4 类:
第一类:不淘汰(仅拒绝写入)
策略名称:noeviction(默认)
逻辑:
- 当内存达到 maxmemory 时,拒绝所有新键的写入请求(返回 OOM command not allowed)
- 允许读取和删除现有键
- 不会删除任何数据
适用场景:
- 不允许数据丢失的场景(如 Redis 作为持久化数据库,而非缓存)
- 当数据比可用性更重要时
- 此时宁愿拒绝写入,也不删除已有数据
示例:
127.0.0.1:6379> SET new_key "value"
(error) OOM command not allowed when used memory > 'maxmemory'.
第二类:淘汰 "过期键"(仅删除带过期时间的键)
这类策略仅针对设置了 expire 的键进行淘汰,未设置过期时间的键不会被删除,适合 "缓存场景"(过期键本身就是临时数据)。
策略名称 | 淘汰逻辑 | 适用场景 | 示例说明 |
---|---|---|---|
volatile-ttl | 淘汰剩余过期时间最短的键(TTL 最小的键) | 希望尽快淘汰即将过期的数据,保留仍有较长有效期的数据 | 键A(剩余5分钟)、键B(剩余10分钟)→淘汰键A |
volatile-random | 从所有带过期时间的键中,随机淘汰一个 | 对淘汰的数据没有优先级要求,追求简单高效 | 从100个过期键中随机选一个删除 |
volatile-lru | 从所有带过期时间的键中,淘汰最近最少使用(LRU)的键 | 缓存场景的经典策略:认为"最近不用的键,未来也大概率不用" | 键A(1天前访问)、键B(1小时前访问)→淘汰键A |
volatile-lfu | 从所有带过期时间的键中,淘汰最近最少频率使用(LFU)的键 | 比 LRU 更精准:不仅考虑"是否最近使用",还考虑"使用频率" | 键A(过去1周访问5次)、键B(过去1周访问20次)→淘汰键A |
第三类:淘汰 "所有键"(无论是否带过期时间)
这类策略会删除所有键(包括未设置过期时间的键),适合 Redis 完全作为 "缓存" 使用,且允许所有数据被淘汰的场景。
策略名称 | 淘汰逻辑 | 适用场景 | 示例说明 |
---|---|---|---|
allkeys-random | 从所有键中(无论是否过期),随机淘汰一个 | 对数据没有访问优先级,追求极致性能(随机淘汰的开销最低) | 从所有1000个键中随机删除一个 |
allkeys-lru | 从所有键中,淘汰最近最少使用(LRU)的键 | 通用缓存场景:假设"最近访问的键,未来更可能被访问" | 键A(3天前访问)、键B(2小时前访问)→淘汰键A |
allkeys-lfu | 从所有键中,淘汰最近最少频率使用(LFU)的键 | 高频访问场景(如热点商品缓存):频率高的键更值得保留,比 LRU 更贴合实际访问模式 | 键A(过去访问100次)、键B(过去访问10次)→淘汰键B |
策略选择建议
- 如果 Redis 仅用作缓存:allkeys-lru 或 allkeys-lfu
- 如果部分数据不能丢失:volatile-* 策略
- 对性能要求极高:random 策略
- 数据完全不能丢失:noeviction
2.3 关键概念:LRU 与 LFU 的区别
LRU(Least Recently Used,最近最少使用)和 LFU(Least Frequently Used,最近最少频率使用)是两种最常用的淘汰算法,也是面试高频考点,必须明确区别:
LRU:基于 "访问时间"
核心逻辑:
- 判断键的 "最后一次访问时间"
- 淘汰最后一次访问时间最早的键(即 "最久没被用" 的键)
- Redis 使用近似 LRU 算法,通过采样来提高性能
示例:
- 键 A(10:00 访问)、键 B(10:05 访问)、键 C(10:10 访问)
- 内存不足时,淘汰键 A
优点:
- 实现简单
- 对大多数缓存场景表现良好
缺点:
- 无法区分 "高频但最近未用" 的键
- 例如:一个每天访问 100 次的键,因凌晨 1 小时未访问,可能被淘汰
- 而一个每天访问 1 次但最近刚访问的键被保留 —— 这不符合实际需求
LFU:基于 "访问频率"
核心逻辑:
- 为每个键维护一个 "访问计数器",记录一段时间内的访问次数
- 淘汰计数器最小的键(即 "用得最少" 的键)
- Redis 4.0 引入
示例:
- 键 A(计数器 100)、键 B(计数器 10)、键 C(计数器 5)
- 内存不足时,淘汰键 C
优化:
- Redis 的 LFU 会 "衰减" 计数器(如每隔一段时间,计数器乘以 0.8)
- 避免 "历史高频但当前低频" 的键一直被保留
- 例如一个键过去高频,但最近 1 个月未使用,计数器会逐渐降低,最终被淘汰
优点:
- 更精准反映实际访问模式
- 特别适合有热点数据的场景
缺点:
- 实现复杂度更高
- 占用更多内存(需要维护计数器)
总结:LRU vs LFU
维度 | LRU | LFU |
---|---|---|
核心依据 | 访问时间(最近) | 访问频率(次数) |
适用场景 | 访问模式较稳定,无明显高频键 | 存在明显高频访问的场景(如热点数据) |
精准度 | 较低(易误淘汰高频但最近未用的键) | 较高(更贴合实际访问需求) |
实现复杂度 | 简单 | 复杂 |
内存开销 | 小 | 较大 |
Redis 版本 | 所有版本 | 4.0+ |
2.4 内存淘汰的执行流程
当客户端发起写入请求(如 SET、HMSET),且 Redis 内存已达 maxmemory 时,执行流程如下:
内存检查:
- Redis 检查当前内存是否超过 maxmemory
- 若未超过,正常执行写入
- 若已超过,进入淘汰流程
策略选择:
- 根据 maxmemory-policy 选择对应的淘汰策略
- 如果策略是 noeviction,直接返回 OOM 错误
数据淘汰:
- 按照策略选择候选键
- 删除选中的键
- 可能需要重复此步骤直到释放足够内存
执行写入:
- 腾出足够内存后
- 执行客户端的写入请求
注意事项:
- 删除数据时,Redis 会触发 "键删除事件"(如 DEL 命令的钩子)
- 如果配置了持久化(RDB/AOF),会同步更新持久化文件(确保数据一致性)
- 淘汰过程是同步的,会阻塞当前客户端请求直到完成
- 在大数据量情况下,淘汰可能影响性能
性能优化建议:
- 合理设置 maxmemory,避免频繁触发淘汰
- 对于大内存实例,考虑使用 allkeys-random 减少淘汰开销
- 监控 evicted_keys 指标(通过 INFO 命令),了解淘汰频率
- 在写入高峰前预热缓存,减少淘汰压力
三、生产环境配置建议
3.1 过期策略相关配置
hz 参数配置优化
Redis 默认的 hz
值为 10,表示每秒执行 10 次后台任务。建议根据业务场景调整:
缓存为主场景
- 典型场景:电商商品缓存、社交平台热点数据缓存
- 当业务中存在大量过期键(如 TTL<24h 的临时数据)时,建议将
hz
调整为 20-30 - 效果:加快过期键清理速度,减少内存碎片(实测可降低内存占用 5-15%)
持久化存储场景
- 典型场景:用户会话存储、订单状态存储
- 若服务器 CPU 利用率常高于 70%,建议保持
hz=10
- 警告:禁止设置
hz>50
,测试显示当hz=100
时 CPU 占用可能飙升 300%
过期时间分散策略
批量设置过期键时,建议采用时间偏移算法:
# 示例:基础过期时间3600秒,随机增加0-600秒偏移
expire_time = 3600 + random.randint(0, 600)
redis_client.expire("key", expire_time)
问题规避:某社交平台曾因百万级键在同一秒过期,导致请求延迟从 2ms 飙升至 1200ms
3.2 内存淘汰相关配置
maxmemory 设置规范
部署方式 | 计算公式 | 示例(总内存) | 建议值 |
---|---|---|---|
物理机 | 总内存×75% | 8GB → 6GB | 留出25%给系统/kernel |
Docker | 容器限制×80% | 4GB限制 → 3.2GB | 需考虑监控组件开销 |
淘汰策略选型指南
缓存场景
allkeys-lfu
:适用于有20%热点数据(如短视频热门内容)allkeys-lru
:适合均匀访问(如企业OA系统文档)- 性能对比:LFU 比 LRU 多消耗约 5% CPU,但命中率高 8-12%
持久化场景
noeviction
必须配合监控告警,当内存达阈值时:# 紧急处理命令示例 redis-cli --bigkeys redis-cli --hotkeys
特殊场景
- 测试环境建议
allkeys-random
,某金融测试显示淘汰速度比 LRU 快 40%
- 测试环境建议
3.3 监控与调优
关键监控指标
# 过期键监控(建议设置<1000/秒)
redis-cli INFO stats | grep -E "expired_keys|expire_cycle_cpu"# 内存健康检查(建议每周一次)
redis-cli INFO memory | grep -E "used_memory|peak|fragmentation"
调优决策矩阵
指标异常 | 可能原因 | 解决方案 |
---|---|---|
expired_keys突增 | 批量键同时过期 | 增加时间随机偏移 |
内存碎片率>1.5 | 频繁修改大key | 重启实例或使用jemalloc |
命中率<90% | 策略不当/内存不足 | 切换LFU或扩容20% |
实战案例:某游戏公司通过调整 hz=25
+ allkeys-lfu
,使缓存命中率从 88% 提升至 96%,月节省 CDN 费用 $12k
四、常见问题与解决方案
Q1:为什么设置了过期时间的键,过期后仍能被访问到?
详细原因分析:
惰性删除机制延迟:
- Redis采用惰性删除策略,只有当某个键被访问时才会检查其是否过期
- 示例:一个设置了10秒过期的键,如果之后20秒内都没有被访问过,它就会一直存在于内存中
定期删除扫描遗漏:
- Redis默认每100ms(HZ=10)随机抽取部分键检查过期情况
- 当数据库键数量庞大时,可能存在扫描遗漏的情况
- 实际测试案例:在100万键的环境中,过期键可能滞留30秒以上
主从同步特性:
- 从节点不会主动删除过期键,依赖主节点的DEL命令同步
- 网络延迟或同步异常时,从节点可能保留过期键长达数分钟
- 典型场景:主节点宕机切换时,新主节点可能包含原主节点未同步的过期键
优化解决方案:
主动维护方案:
- 定期执行SCAN+TTL命令扫描过期键
- 使用Redis的
CONFIG SET hz 100
提高定期删除频率(需权衡CPU消耗)
架构层面改进:
- 对从节点设置
replica-serve-stale-data no
,避免返回过期数据 - 考虑使用Redis Cluster,分散键空间压力
- 对从节点设置
监控预警:
- 通过INFO命令监控expired_keys指标异常增长
- 设置内存使用率超过80%的告警阈值
Q2:内存淘汰时,Redis 会阻塞客户端请求吗?
阻塞机制深度解析:
不同淘汰策略的阻塞时间:
策略类型 阻塞时间 关键影响因素 allkeys-random 10-50μs 哈希表查找速度 volatile-lru 1-5ms 采样数量(pool-size) allkeys-lfu 2-10ms 计数器衰减频率 极端情况分析:
- 当内存超限超过50%时,Redis可能需要进行多轮淘汰
- 测试数据:100万键的环境下,allkeys-lru策略可能导致100ms级别的阻塞
内核参数影响:
- Linux的Transparent Huge Pages(THP)会导致更大的阻塞波动
- 建议设置
echo never > /sys/kernel/mm/transparent_hugepage/enabled
生产环境优化建议:
策略选择优先级:
- 首选:allkeys-lfu(Redis 4.0+)
- 次选:volatile-ttl(如果有明确的过期时间设置)
- 避免:noeviction(可能导致写操作完全阻塞)
参数调优方案:
# 设置内存淘汰阈值(建议物理内存的70-80%) config set maxmemory 16gb # 调整LRU采样精度(默认5,可提升至10) config set maxmemory-samples 10
监控指标:
evicted_keys
:监控淘汰键数量突变blocked_clients
:关注阻塞客户端数量
Q3:LFU 策略的计数器如何衰减?
计数器工作机制详解:
衰减算法实现:
- 衰减公式:
new_counter = current_counter * 0.8
- 特殊处理:计数器最大值为255,衰减后小于5则直接置0
- 源码示例(简化版):
if (counter > 0) {counter = (counter * 4) / 5; // 等价于乘以0.8if (counter < 5) counter = 0; }
- 衰减公式:
访问频率权重计算:
- 新建键初始计数器=5(避免立即被淘汰)
- 每次访问时的增长公式:
增量 = 1 / (0.1 * counter + 1) 新值 = min(当前值 + 增量, 255)
实际应用场景:
- 热点数据:频繁访问的计数器可维持在200+
- 温数据:周期性访问的计数器在50-100波动
- 冷数据:计数器会快速衰减至0
运维管理建议:
监控调优方案:
# 查看LFU相关统计信息 redis-cli --lfu-stats # 手动触发计数器衰减 redis-cli INFO | grep lfu
参数调整方法:
# 调整计数器对数因子(默认10,数值越大衰减越快) config set lfu-log-factor 12 # 调整计数器初始值(默认5) config set lfu-decay-time 1
最佳实践:
- 对突发流量场景,可适当降低lfu-log-factor
- 对长期稳定业务,建议保持默认参数
- 避免频繁执行INFO命令,建议间隔至少60秒