1. 先把两个策略的边界划清楚:一个管过期,一个管满员
很多人在刚接触Redis的时候,很容易把“过期策略”和“淘汰策略”搅在一起。面试时候被问到“Redis内存满了会怎么样”,经常有同学张口就说“把过期的key删掉”,这个答案其实只答对了一半,甚至可以说没答到点子上。
我的理解是这样:Redis有两条独立的内存管理线,一条管“key到了过期时间之后的清理”,另一条管“内存已经写满时对新写入的处理”。它们虽然最终都是在释放内存,但触发条件、执行路径、配置参数完全不同。
- 过期策略(Expire):针对设置了过期时间的key(TTL不为-1),目标是让dead key按时消失,避免占着内存不放。
- 淘汰策略(Eviction):没有设置过期时间的key,或者过期的key还没来得及清理导致内存达到maxmemory上限时,Redis必须主动踢掉一些旧数据,才能让新数据写进来。
另一个容易忽略的点是,过期策略不是只做一次就完事,它有两层配合,后面我会详细拆。而淘汰策略是在maxmemory这个红线上做文章,两种策略合起来才是Redis在有限内存里长期稳定运行的关键。
实话说,单纯背概念很容易,但真正要命的是实际生产环境里的组合拳:什么场景下过期策略会失效?淘汰策略选错了会有什么后果?这些问题才是今天这篇想跟大家说透的东西。
2. 过期策略的三层机制拆解:惰性删除、定期删除、淘汰策略的兜底
Redis官方文档里对过期删除的描述非常简洁,但实际代码逻辑分三条线。我按执行时机逐个讲。
2.1 惰性删除:平时不动手,读的时候才检查
当客户端发起一个GET、SETEX、INCR这类命令时,Redis内部会调用一个expireIfNeeded函数,逻辑是:
- 取key的过期时间(如果有)
- 如果当前时间已经超过了过期时间点
- 删除这个key,并向客户端返回空结果(或者执行键不存在时的分支逻辑)
这就是惰性删除——它不主动扫描、不占额外CPU,只有在被访问的那一瞬间才判断是不是过期了。优点是省CPU,缺点是如果某个key过期后再也没人访问,它会一直躺在内存里,这就是常说的“内存泄漏”隐患。
举个例子,一个缓存key设置TTL是24小时,如果业务来源是一次性活动,活动结束后没人再读这个key,它就会在Redis里长期占着内存,永远不会被惰性删除触发。
2.2 定期删除:Redis自己有个后台巡视任务
为了处理上面那种“没人访问就一直留着”的情况,Redis实现了定期删除机制。核心代码在activeExpireCycle,它每隔一段时间(由hz参数控制,默认10)就会抽样检查一批设置了过期时间的key。
这个逻辑有几个关键设计:
- 不是全量扫描,而是从过期key集合中随机抽取一部分,批量检查
- 有执行时间预算:每次循环最多运行一定毫秒(默认可以跑1ms,可配置)
- 每次循环结束会记录游标位置,下次从上次的地方继续,保证最终所有过期的key都会被扫到
一句话总结:定期删除是惰性删除的补强机制,用可控的CPU代价降低过期key残留的几率。
2.3 为什么还需要淘汰策略兜底?
这里有个很多人没想透的问题:既然有过期策略,理论上过期的key最终都会被清掉,为什么还要讨论淘汰策略?
答案是你可能永远等不到那条清理路径。两个典型场景:
- 大量短TTL的key同时涌入,定期删除扫不过来,惰性删除又没人读,内存瞬间被打满
- 大量key本身没有过期时间,或者过期时间设得特别长,内存增长完全由业务并发量驱动
这两种场景下,Redis必须有一套在“内存满了,但新数据还要写”时如何取舍的规则——这就是淘汰策略的意义。所以淘汰策略不是和过期策略并列的替换方案,而是在过期策略来不及处理的窗口期,兜住内存红线的最后一道防线。
3. 内存淘汰策略的完整图谱:8个配置逐个说
maxmemory-policy这个配置项一共支持8个值,我按从简单到复杂的顺序给它们分组。
3.1 不做淘汰:noeviction
这是Redis默认的策略:内存达到maxmemory之后,所有写命令(SET、LPUSH、SADD等)会直接报错,业务层会看到类似OOM command not allowed when used memory > 'maxmemory'的错误。读命令不受影响。
这个策略适合把Redis当纯缓存且内存余量充足的环境,或者你认为Redis里的数据全都不可丢、丢任何一个都宁可不写新数据的场景。但实际生产里,我基本不推荐用noeviction,因为一旦内存临界点被触发,业务写入立刻全部失败,连锁反应很容易击垮上游服务。
3.2 在全体key范围内淘汰
这组策略对所有key一视同仁,不区分你有没有设置过期时间:
allkeys-lru:按照LRU(最近最少使用)算法淘汰最久没被访问的keyallkeys-lfu:按照LFU(最不经常使用)算法淘汰访问频率最低的keyallkeys-random:随机淘汰key
3.3 只在设置了过期时间的key范围内淘汰
这组策略只对“生死有期”的key生效,永久key不受影响:
volatile-lru:在设置了过期时间的key里,用LRU淘汰volatile-lfu:在设置了过期时间的key里,用LFU淘汰volatile-random:在设置了过期时间的key里随机淘汰volatile-ttl:在设置了过期时间的key里,优先淘汰剩余TTL最短的
注意一个细节:如果你设置了volatile-*策略,但Redis里几乎都是没有过期时间的key,那当内存满了、又没有可淘汰的volatile key时,Redis的行为会退化到noeviction——直接拒绝写入。这是生产环境最常见的误配之一。
3.4 LRU和LFU的区别,以及近似LRU机制
严格说,Redis的LRU不是传统意义上的全量LRU,它用的是一个近似LRU算法。Redis会给每个key记录一个24位的lru时间戳(精确到秒),在需要淘汰时,从所有key里随机抽取一批(maxmemory-samples控制采样数量,默认5),在这批样本里挑出最久没被访问的那个淘汰掉。
这就是著名的“采样淘汰”思路:不维护全局LRU链表,只用采样逼近真实LRU。带来两个好处,一是内存开销极小,二是性能稳定。缺点是它不是绝对精确:新写入的key如果运气不好被采样到并选中,可能会被误淘汰。
LFU则是在LRU基础上增加了一个访问频率计数,用Morris计数器近似实现(用非线性的方式记录访问次数,避免溢出)。它对“一个key平时不怎么用、偶尔被大批量访问”的场景判断更准,更适合突发业务。
| 策略 | 适用场景 | 核心缺点 |
|---|---|---|
| noeviction | 纯缓存且内存充足 | 内存满后写失败 |
| allkeys-lru | 各种key都愿意丢弃的通用缓存 | 可能误淘汰刚写入的重要key |
| volatile-lru | 只希望淘汰可再生的缓存key | 如果volatile key不足会退化 |
| allkeys-lfu | 访问频率差异极大的业务 | 新key初期访问次数少容易被淘汰 |
| volatile-ttl | 明确设置了有效期的缓存数据 | 剩余时间最短的不一定是最该删的 |
4. 关键配置项与选型决策:maxmemory怎么设、采样数调多少
4.1 maxmemory内存上限怎么算
maxmemory决定Redis在什么水位触发淘汰。这个值不是越大越好,关键是给操作系统留下运行余地。
我的经验是:maxmemory = 物理内存的 55%~65% 左右,剩下的留给系统、Redis自身进程、fork子进程(RDB持久化时)以及网络缓冲区。如果你用的是云服务器,还要考虑同一台机器上是否有其他进程。
另外,Redis自身还有一个used_memory概念,你可以用INFO memory查看,里面能看到used_memory、used_memory_rss、mem_fragmentation_ratio(内存碎片率)等关键字段。设置maxmemory之前先观察几天,看清业务实际占用,再留20%~30%的余量,比拍脑袋定数字靠谱得多。
4.2 maxmemory-samples采样数
maxmemory-samples影响LRU和LFU的淘汰精度。默认5,调到10可以让近似LRU更接近真实LRU,但代价是每次淘汰要比较的样本更多、CPU消耗略微上升。
我的建议是:如果key数量特别大(比如千万级以上),保持默认5就够了;如果内存相对紧张、淘汰比较频繁,可以调到10,实测对CPU的影响很微小。
4.3 策略选型:结合具体场景的经验值
下面是我在项目里总结出的几种选型套路,不是标准答案,但可以直接抄作业:
- 纯缓存场景,所有key都是可再生成的:首选
allkeys-lru。热点数据天然被保留,冷数据被优先淘汰,业务也感知不到。 - 缓存+持久化数据混存,持久化key不可丢:用
volatile-lru或volatile-ttl,并且保证所有可淘汰的key都设置了过期时间。光靠“我不用永久key”的自觉是不够的,最好在写入时统一封装一个强制过期时间的工具方法。 - 访问频率分布非常不均匀,极少数key扛着绝大部分流量:
allkeys-lfu比LRU效果好,但要关注新key首次写入后访问量还没起来时被误淘汰的风险。 - 数据文件有明确的生命周期(比如每24小时一轮任务生成的临时数据):
volatile-ttl配合EXPIRE设置,到点自然淘汰,逻辑最简单。
配置修改方式,运行时可以动态调整:
# 通过命令直接修改,临时生效 redis-cli config set maxmemory-policy allkeys-lru redis-cli config set maxmemory 2gb # 如果要持久化,继续执行 redis-cli config rewrite注意config rewrite只会把变化写入redis.conf,如果配置项是从启动命令行带入的,rewrite同样会覆盖掉。
5. 实战中的血泪经验:过期和淘汰策略引发的三类事故
讲理论容易,但我更想把踩过的坑写出来。以下是三个真实发生在生产环境、且都和过期/淘汰策略相关的问题。
5.1 事故一:大量key在同一秒过期,缓存雪崩
以前给一个电商项目做活动页缓存,当时缓存策略是按“活动开始时间+固定偏移量”设置TTL的。活动零点开启,所有相关key的过期时间都被设置成当天的结束时间,结果第二天零点一过,几千个key在同一秒被标记过期,缓存层瞬间全部miss,数据库在那一秒被打到CPU报警。
这个问题的根源不是过期策略本身,而是过期时间设置太集中。解决方式很简单:对相同业务类型的缓存加上随机偏移量。
# Python示例:设置随机TTL避免雪崩 import random base_ttl = 24 * 60 * 60 ttl = base_ttl + random.randint(0, 600) cache.set(key, value, ex=ttl)如果你用的是批量设置过期时间的逻辑,同理给每个key加一个随机尾部。注意不要把随机范围设太大,否则有些key过早失效,反而增加缓存穿透概率。
5.2 事故二:大key过期阻塞Redis主线程
有一次用户反馈某个查询突然变慢,排查后发现是Redis主线程耗时飙升。定位到最后,是一个存了上百万元素的zset key过期,惰性删除逻辑在主线程里执行删除操作,删除大key释放内存的动作让主线程阻塞了整整几百毫秒。
这个坑很深:惰性删除和定期删除都是主线程在跑,如果被删除的key非常大,释放内存会直接卡住所有命令。我记得Redis 4.0之后有个异步删除命令UNLINK,但过期删除默认还是同步的。后来我们的应对方式是:
- 对可能变大的value类型(list、zset、hash)单独评估,超过一定规模就不设短TTL,改成异步删除配合业务层清理
- 把关键命令的慢查询日志(
SLOWLOG GET)打开,随时盯着
这个问题在Redis 6.0以后有所改善(内部增加了异步释放机制的演进),但贫血的建议仍然是:别让大key过期,这是底线。
5.3 事故三:volatile-lru遇到大量永久key,服务直接只读
有次给一个内部系统上线新版时,负责Redis的同学把maxmemory-policy设置成了volatile-lru,但没有检查已有key的过期设置。结果发现业务代码里有一部分数据写的是不带过期时间的(比如用户配置类数据)。内存打满后,volatile-lru找不到可淘汰的key,直接退回noeviction模式,所有写操作全部报错,整个系统进入只读状态。
后面我们归纳出一个规律:
- 凡是使用
volatile-*策略,一定要配套监控“当前有多少key是不过期的”,或者干脆写一个定期扫描任务,把过期集合为空的情况实时暴露出来 - 更稳妥的办法是用
allkeys-lru,它的淘汰目标永远充足,很少出现系统因为无法淘汰而“罢工”的问题
6. 状态观测与调优手法:怎么确认策略在正常工作
光配置对还不够,还要能随时看清Redis在做什么。我来分享几个我自己最常看的指标和命令。
6.1 核心指标
用INFO memory可以拿到:
used_memory和used_memory_human:实际占用的逻辑内存maxmemory和maxmemory_human:内存上限mem_fragmentation_ratio:内存碎片率,值在1~1.5之间比较健康,超过1.5说明碎片有点多,可以考虑重启或设置activedefrag yesevicted_keys:由于淘汰而被踢掉的key数量,这个数字是评估淘汰是否频繁的直接依据
用INFO stats可以看到expired_keys(过期被删除的key数量)、evicted_keys(淘汰掉的key数量),以及keyspace_hits和keyspace_misses。
6.2 通过监控判断策略是否合适
如果evicted_keys持续上涨,说明内存一直处于满负荷状态,这时候要看淘汰的对象是不是业务核心数据。我一般这样判断:
evicted_keys上涨但keyspace_hits稳定:淘汰的key是低频冷数据,基本健康evicted_keys上涨同时keyspace_hits明显下降:说明在淘汰热数据,策略选错了,或者maxmemory设得太低expired_keys极少、used_memory却一直增长:说明定期删除扫不到,比如key没有设置过期时间,但策略却选了volatile系列
有一条命令可以快速查看所有key的TTL分布情况:
redis-cli --bigkeys虽然这个命令主要用来找大key,但它也会输出不同类型key的分布。查过期情况,可以写个简单的scan脚本,按比例抽查TTL。
6.3 动态观测和临时调整
线上排查时可以这样操作:
# 查看当前策略 redis-cli config get maxmemory-policy # 临时切换(不用重启) redis-cli config set maxmemory-policy allkeys-lru # 确认改动是否已写入配置文件 redis-cli config get maxmemory-policy注意生产环境的改动要留审计记录,不能只靠现场操作。我习惯在切换策略前用监控工具截图保存,方便事后复盘。
7. 几个更容易被忽略的边界问题
7.1 过期时间与主从复制的不一致
在Redis的主从架构中,主库上的过期key删除之后会向从库同步一条DEL命令,从库自己也会维护一份过期字典,来保证读请求不会读到已过期的数据。这里有一个老版本里的坑:在主从网络分区期间,从库如果处理过期逻辑不当,可能短暂返回过期数据。
Redis的解决方式是:从库不会单独执行过期删除,而是依赖主库的DEL命令或者自身的时钟判断。注意,如果主库长期不可用,从库时钟和主库不一致,也可能出现数据不一致的情况。我没法在这里覆盖所有边界,但建议在高一致性要求的场景下,监控主从的master_repl_offset和延迟指标。
7.2 淘汰策略不会保证“最不可能用的key一定被淘汰”
近似LRU毕竟是采样机制,理论上存在淘汰错对象的情况。对数据可靠性要求极高的场景,不要依赖淘汰策略来兜底,应该靠容量规划和限流来保证内存永远不触线。
7.3 Redis 4.0之后的内存碎片整理
activedefrag yes可以在内存碎片率达到阈值时自动开启整理。但它和淘汰策略相互独立,生产环境开启前要先做压测,因为整理过程本身会占用CPU。我一般建议在碎片率超过1.5时才主动启用,平时保持默认关闭。
8. 个人总结与经验补遗:这套体系我在线上是怎么用的
说到底,过期策略和淘汰策略不是面试题里的两条背答案,而是在容量规划、故障演练、监控体系里都要给出明确回答的设计决策。
以我维护过的几个业务为例,网上最常见的模板配置是:maxmemory 2gb+allkeys-lru+maxmemory-samples 10。对这个方案我的看法是:适合大多数中小型缓存场景,但并不是最优解。如果业务里存在少量绝对不能丢的数据(一些公司会把部分配置态数据也放Redis),那我会改成volatile-lru,并且强制所有可淘汰key的写入路径都带TTL,同时跑一个巡检脚本:
import redis r = redis.Redis.from_url('redis://localhost:6379') # 抽查1万个key中设置过期时间的比例 cursor = 0 total = 0 expired = 0 for _ in range(100): cursor, keys = r.scan(cursor=cursor, count=2000) if not keys: break total += len(keys) for k in keys: if r.ttl(k) != -1: expired += 1 print(f"scan {total} keys, {expired} have TTL, ratio: {expired / max(total, 1) * 100:.2f}%")如果脚本发现volatile比例接近100%,那用volatile-lru才真正安全;如果比例偏低,说明配置和实际数据形态不匹配,早晚要出事故。
最后分享一个我自己反复琢磨的结论:过期策略关注的是时间,淘汰策略关注的是空间,但线上稳定性关注的是两者的配合节奏。最好的状态不是天天触发淘汰,而是让过期key在定期删除中自然消失,淘汰策略只作为极少数瞬间的缓冲。如果你发现生产环境每天都在大量淘汰key,第一反应不应该是调大maxmemory,而是检查key的过期时间设置是否合理、缓存的数据量是否超出了当初的规划。
Redis的内存管理之所以值得花时间吃透,是因为它直接决定了你在高并发下到底是“闪电”还是“慢速爬行”。把这些机制在自己脑子里构建成完整闭环,再遇到缓存雪崩、内存打满、只读故障,你就能快速定位到正确的层,而不是在错误的方向上绕远路。