☰
Redis内存淘汰策略与大Key治理:8种策略选型与优化实践
2026/10/22 18:46:25 网站建设 项目流程

1. 内存淘汰的本质:Redis快满时会发生什么

先讲一个比较典型的场景。某个业务实例突然出现大量写入超时,监控面板上内存使用率已经逼近上限,但日志里看不到任何崩溃信息。我登录上去敲完redis-cli -p 6379 info memory之后,意识到这是典型的“内存满了但没人管”的局面:used_memory已经非常接近maxmemory,而配置的淘汰策略又过于保守,Redis只能僵硬地拒绝一切会占用内存的写操作。

要理解Redis内存淘汰策略,不能把它当成一个简单的“缓存清理开关”。Redis核心是帮业务扛住高频读写,它的所有响应都发生在主线程上,内存分配、释放、复制也都在主线程上。一旦内存触顶,系统又找不出可以安全清理的key,接下来就是连锁反应:写入报错、主从堆积、回源爆炸。

1.1 内存模型里被低估的“隐性占用”

在配置maxmemory之前,很多人对Redis内存的认知是“存了多少数据就占多少内存”。这话对,但也不全对。Redis的内存模型里,除了数据value本身,还包括key的开销、dictEntry节点、对象头、客户端输出缓冲区、复制积压缓冲区,以及内存碎片。

举一个印象很深的例子。当时有个团队在Redis里放了大量Set类型的key,每个key下挂几十万个元素,业务反馈说“明明数据量只有300MB,但实例内存已经2.5GB”。原因在于Set底层在元素数量多的时候会从intset升级到hashtable,每个dictEntry都要额外分配指针和元数据,逻辑数据体积和真实内存占用完全不是一个数量级。

所以,内存淘汰策略真正要处理的,不只是“用户看到的数据”,而是整个Redis进程在maxmemory边界附近的总占用。只有理解了这一层,才明白为什么设置maxmemory不能拍脑袋,也不能只在OOM之后才想起配置。

1.2 不设置maxmemory的隐患

很多新手以为“不设置maxmemory就不会内存超限”,这是一种错觉。64位系统里,Redis在没有配置maxmemory的情况下,理论上可以用无限多的内存,直到操作系统物理内存被吃满。真到那个地步,系统的OOM机制会直接挑一个进程杀掉,Redis往往就在候选名单的前面。实例被杀死之后,如果架构里有主从和哨兵,会发生切换,新节点又要重新加载全量数据,又可能把内存撑上去,反反复复,形成一个死循环。

所以在生产环境,maxmemory一定要配。设置了maxmemory之后,Redis才对内存有了“边界感”,才会在超出边界时启动淘汰机制。不要觉得淘汰是坏事,它其实是Redis自我保护的最后一道底线。问题只在于:该淘汰哪些key,用什么算法来选。

1.3 noeviction默认策略的两面性

默认情况下,Redis的淘汰策略是noeviction。这个策略的意思是:不淘汰任何key,内存写满之后,所有会导致内存继续增长的写命令都会报错。很多人第一反应是“这策略太弱了,生产环境肯定不能用”,但实际它保护了一批业务的语义:不让任何一个已经写入的key被悄悄清掉。

noeviction也有个隐蔽问题:主从架构下,master写不进去,replica自然也没法同步数据,整个副本链路都会停在某个时间点。也就是说,它不只是影响写入方,还会影响数据复制链路。所以哪怕业务数据探入所有key都必须保留,也不能只靠noeviction,还是得配合maxmemory的合理规划和必要的过期时间设计。

2. 八种淘汰策略拆解:怎么选、区别在哪

Redis从4.0之后,主流版本里可以配置的淘汰策略一共有8种:noeviction、volatile-lru、allkeys-lru、volatile-lfu、allkeys-lfu、volatile-random、allkeys-random、volatile-ttl。看起来多,实际核心思路归纳下来就三类:不淘汰、只淘汰设置了过期时间的key、所有key一起参与淘汰。

八种策略对比

策略淘汰范围算法是否看过期时间典型场景
noeviction无(写满拒绝写入)无否强一致缓存、不允许丢数据
volatile-lru设置了过期时间的key近似LRU是临时缓存 + 长期key混合
allkeys-lru所有key近似LRU否纯缓存、数据可全部丢弃
volatile-lfu设置了过期时间的key近似LFU是数据热度差异大的临时缓存
allkeys-lfu所有key近似LFU否高频与低频差异明显的缓存
volatile-random设置了过期时间的key随机是临时数据,淘汰谁无所谓
allkeys-random所有key随机否数据完全同质、可任意丢弃
volatile-ttl设置了过期时间的key按剩余TTL是希望更早过期的先被清掉

2.1 按“淘汰范围”分两大类

volatile-前缀的策略有个共同特点:只会在设置了过期时间的key里挑淘汰对象。那些没有过期时间的key,相当于持有“免淘汰金牌”,无论如何都不会被选中。allkeys-前缀则是全员参与,不管你有没有过期时间,只要内存不够了,都有可能被淘汰。

这个前缀差异,在选型时是决定性的。如果你在同一个实例里既放了用户购物车数据,又放了用来做配置读取的核心参数,配置参数不设过期时间,那么用volatile-系列能保证核心参数不被淘汰,购物车数据就算没了也有备份。反过来,如果用了allkeys-lru,核心配置同样可能被挤掉,导致业务行为异常。

2.2 LRU近似算法的取舍

很多人以为Redis的LRU和操作系统的LRU一样精确,其实不是。Redis用了“近似LRU”的思路:不维护全局访问顺序链表,而是在候选池里随机采样一小部分key,根据采样结果挑出最久未使用的那个。默认采样数量是5,相关参数是maxmemory-samples。

这个设计是从CPU和内存开销角度出发的。真实LRU需要在每次访问时调整顺序,在Redis这种单线程模型里会带来不小的开销。近似LRU牺牲了一点精度,换来了极高的性能。实际生产数据里,采样数为5时,淘汰结果已经很接近真实LRU,除非你遇到极度追求精确的场景,否则我都不建议把采样数拉得太大,设置成10以上对性能的影响就开始显现了。

2.3 LFU的频率计数与衰减

LFU和LRU的区别,一句话可以讲清楚:LRU看“多久没访问”,LFU看“最近一段时间访问了多少次”。Redis的LFU实现更巧妙,它不会真正记录无限的访问次数,而是用一个对数递增计数器,把高频key的计数增长变缓,同时按时间周期做衰减。

这种设计的实际价值是:能防止“突发热点数据挤掉长期高频数据”。比如某个推荐位在活动期间被爆量访问,如果使用allkeys-lru,它可能把原本稳定高热的商品列表顶掉。而Use allkeys-lfu后,Redis会优先淘汰“访问频率最低”的key,而不是“最近没人访问”的key。

2.4 volatile-ttl与随机策略的适用场景

volatile-ttl的意思是,在设置了过期时间的key里,优先淘汰剩余存活时间最短的。听起来比LRU更“讲道理”,但Redis实现它时同样有采样问题,而且TTL最短的key不一定是最该淘汰的,还要看它的访问热度。所以这个策略在真实业务里用得不多。

volatile-random和allkeys-random适用于数据完全同质的场景,比如一批只有一次性使用价值的临时数据,淘汰哪个都无所谓。但我几乎没见过几个业务能真正做到数据同质,大多数时候,看似同质的数据里总藏着几个访问频率特别高的热点key。如果用了random,热点key也可能被随机选中干掉,对下游就不那么友好了。

2.5 动态配置与持久化

熟悉配置命令是基本功。Redis支持运行期动态调整,不需要重启。

# 查看当前策略和内存上限 redis-cli config get maxmemory-policy redis-cli config get maxmemory # 动态修改,立即生效 redis-cli config set maxmemory-policy allkeys-lru redis-cli config set maxmemory 4gb # 把运行期配置写回配置文件 redis-cli config rewrite

config set可以避免重启,但重启之后如果不做config rewrite,改动的配置会丢失,回到配置文件里的旧值。我见过不少团队在线调试时改了maxmemory-policy,当时观察效果没问题,结果下一次发布重启Redis,策略悄悄回了默认值,最终引发内存写满事故。

3. 生产环境选型经验:策略不是越激进越好

选型的时候,很多人第一反应是“那就allkeys-lru吧,省心”。我一开始也这么干过,后来被现实打脸了。allkeys-lru的实际语义是允许丢失任意key,如果业务里有不能丢的核心配置,这个选择就会带来大问题。

3.1 从一次活动流量导致的缓存雪崩说起

有一年大促活动,突发流量把大量热点商品数据塞进了一个共享缓存实例,当时用的就是allkeys-lru。内存快满时,一批没有设置过期时间的核心配置key被淘汰掉了,那批配置恰好承载着动态权限逻辑。结果权限读取失败,所有请求开始回源数据库,数据库连接数瞬间被打满,整个服务进入雪崩状态。

复盘时得出结论:核心配置类缓存和业务列表缓存必须分离部署。如果非要共用同一实例,策略就改用volatile-lru,并给核心配置key设置一个很远的过期时间,让它们至少能进入“可淘汰候选池”,但又因为访问频繁而逃过被淘汰的命运。淘汰策略永远只是最后一道防线,真正的内存治理还是要靠容量规划。

3.2 策略选型对照表

业务状态推荐策略原因
数据允许全部丢弃,回源成本低allkeys-lru内存利用率最高
部分key不能丢,需要保护volatile-lru不设过期时间的key不被淘汰
热点非常集中,回源成本高volatile-lfu / allkeys-lfu低频key优先被淘汰
任何key都不能丢noeviction + 严格容量规划宁可写失败,不能静默丢数据
数据同质,淘汰谁无所谓allkeys-random实现简单,但对冷门热点不友好

还有一个容易被忽略的点:使用allkeys-lru时,即便某key设了很短的过期时间,如果内存不足,Redis也可能在它自然过期前就把它淘汰掉。这就可能导致“缓存过期重算”逻辑失效,因为回源成本高,但key又被提前淘汰,下一次请求还得再算一遍。

3.3 配套监控指标

配置了淘汰策略只是起点,关键是观察它是否按预期工作。我习惯在生产环境里盯四个指标,任何一个出现突变,都会触发排查。

指标数据来源重点关注信号
内存使用率INFO memory 的 used_memory / maxmemory超过80%开始关注
淘汰key数量INFO stats 的 evicted_keys数值骤增说明容量紧张
写入拒绝次数INFO stats 的 rejected_connections有值就必须处理
过期key数量INFO stats 的 expired_keys排查业务过期设置是否合理

很多团队只盯着CPU和内存使用率,对evicted_keys完全不设告警,结果等到Redis把一批key无声无息淘汰掉,业务数据开始错乱才反应过来。evicted_keys本身就是内存压力的直接体现,建议单独做一条监控曲线。

4. 大Key问题的完整识别链路

再说说大Key。先纠正一个常见误解:大Key不是“占用内存超过某个值的key”这么简单,它的危害不在于体积本身,而在于单次操作耗时、网络传输量、内存碎片和主从同步放大效应。

4.1 大Key阈值怎么定

业界没有一个绝对统一的数字,我的参考标准比较保守,适合大多数业务起步:

  • String类型:value超过10KB,就可以视为潜在的大Key。
  • Hash/Set/ZSet/List类型:元素个数超过10000个,或总大小超过1MB,都要重视。

为什么是10KB?因为这个量级的value在GET时,如果遇到带宽紧张或网络抖动,单次传输已经可能成为瓶颈。集合类型的10000个元素,无论是hgetall还是smembers,一次性取出时,响应体都会明显变大,而且Redis需要逐项拷贝数据,主线程耗时直线上升。

4.2 单线程模型下的三个后果

大Key带来的第一个问题是阻塞。Redis是单线程模型,操作大Key时,从内存分配到数据返回的整条链路都占用主线程。比如对一个有几百万个元素的Hash执行hgetall,Redis需要逐项遍历底层哈希表,组装响应,这段时间内其他请求只能排队等待,整体延迟被瞬间拉高。

第二个问题是网络突发。大Key的响应包动辄几MB,批量操作甚至会瞬间打满网卡,带宽被一群大体积响应占空。

第三个问题是持久化和主从同步放大。bgsave触发fork时,子进程复制内存页表,如果内存中堆着多个大Key,fork的开销会被放大。AOF重写也一样,一个大Key只要发生变化,可能会被反复写入多个命令,磁盘IO经常被打满。

4.3 bigkeys扫描的局限与scan脚本姿势

很多资料会告诉你用redis-cli --bigkeys找大Key,但这命令有两个问题:它只输出每个类型里“最大”的那几个,属于抽样统计,不代表找出全部大Key;它是在线扫描,数据量特别大时同样会有压力,而且集群模式下需要逐台执行。

更可控的方式是自己写scan脚本。下面这个思路适用于String类型,用scan分批遍历,比一次性keys *要温和得多。

redis-cli -h 127.0.0.1 -p 6379 --scan --pattern '*' | while read key; do size=$(redis-cli -h 127.0.0.1 -p 6379 strlen "$key" 2>/dev/null) if [ "$size" -gt 10240 ]; then echo "$key $size" fi done

如果是Hash、ZSet这类集合类型,不能用strlen统计。需要先type判断类型,再分别用hlen、scard、zcard、llen查看元素个数。线上执行时,建议选择低峰期,并且每次scan的游标步进不要太大,避免一口气扫太多。

我的习惯是:先跑一轮全量识别,生成大Key名单,再针对名单逐个用debug object key查看序列化长度和编码类型。debug object会带回显,但不适合在循环里频繁调用,因为它也可能造成不必要的CPU消耗。

5. 大Key处理实战:拆分、压缩、安全删除

识别出大Key后,最怕的是直接del。有人觉得“既然它太大,删掉不就行了”,但在主从或集群架构下,del一个大Key同样会阻塞主线程,因为删除集合类型需要逐个释放元素内存,耗时不比读取短。

5.1 集合类型按桶拆分

拆分没有标准答案,但有明确原则:把“大集合”变成“多个小集合”,让单个key的规模降下来。

对于Hash,常见做法是固定桶数拆分。举个例子,原来一个hash存了10万条用户属性,可以拆成100个hash,按用户id的后两位作为分桶后缀:user:attrs:00、user:attrs:01……这样单个key的元素数降到1000左右,原先的一次hgetall可以改成按桶hscan处理。

List类型适合按时间或ID范围分片。比如流水记录,新数据和旧数据分到不同key,旧数据可以定期归档到冷存储,避免一个key无限增长。

ZSet可以按分数段拆分,比如按时间戳范围分片。这样ZRANGEBYSCORE只需访问对应时间段的切片,单key规模可控,查询性能也更稳定。

5.2 序列化与压缩

有些大Key是业务自己撑大的。典型情况是:把一整个对象直接序列化成JSON字符串塞进String,字段多、嵌套深、重复信息多。这种场景下,先考虑业务层改造:

  • 用protobuf或msgpack替换JSON,数据体积能缩小一半以上。
  • 只把热字段放进缓存,冷字段放到数据库或对象存储。
  • 如果是大段文本,可以用gzip压缩后再存入,读取时解压,用CPU换内存。

压缩看起来是技术活,其实策略很简单:先看看value里有没有重复模式,有没有大量可被压缩的空白、JSON键名。有的话,压缩收益非常明显。但要注意,压缩和解压本身会消耗CPU,如果业务对延迟非常敏感,要压测后再决定阈值。

5.3 异步删除与分批删除

Redis 4.0之后提供了unlink,它把释放内存的工作交给后台线程,主线程立刻返回。升级到高版本的人往往还习惯性用del,这就是白白浪费了一个好工具。

# 异步删除,不阻塞主线程 redis-cli unlink big_key_name

如果实例版本老,不支持unlink,那就手动分批删。对Hash、Set、ZSet、List,用hscan、sscan、zscan、lrange一点点取出并删除,每次控制在100到500个元素之间。下面是一个循环删除的思路:

# 对ZSet,按分数范围分批删除,每次取100个 while true; do batch=$(redis-cli zrangebyscore big_zset 0 9999999999999 LIMIT 0 100) if [ -z "$batch" ]; then break; fi for member in $batch; do redis-cli zrem big_zset "$member" done done

真实脚本里还要注意把key名、端口、密码等做成参数,加上超时重试。另外,长时间运行的删除脚本要在业务低峰期执行,同时观察used_memory的下降速度。一个大Key有几百万个元素时,unlink后内存也不会立刻变少,它是逐步释放的,这点不要误判为删除没生效。

6. 内存淘汰与大Key治理的联动

这一节想说的是:内存淘汰和大Key不是两个孤立的问题,它们会互相放大。

6.1 淘汰大Key时的二次阻塞

当内存达到maxmemory触发淘汰时,如果候选key里恰好有大Key,Redis要逐个释放它内部元素的内存,这个过程同样会阻塞主线程。换句话说,大Key不只是静态的体积问题,它在淘汰发生时还会再“蹦”出来阻塞一次。

所以我的建议是:在配置淘汰策略之前,先把大Key名单拉出来,优先处理体积排名靠前的几个。否则,每次内存告警时Redis都会因为淘汰大Key而卡顿,表现就是延迟曲线出现“锯齿状”波动,甚至直接影响核心接口。

6.2 容量规划与规范

大Key和内存淘汰只是两个症状,根因往往在设计阶段就没有做容量评估。我给团队定的基本规范大致是:

  • 所有key必须设计过期策略,不能无限增长不加约束,特别是List、ZSet这类容易滚雪球的类型。
  • 新增缓存结构时要评估value上限,review时把大Key风险写进检查单。
  • 每个实例的maxmemory设置要提前规划,预留30%左右的安全空间,不要顶着上限跑。
  • 淘汰策略必须在实例级别写清楚,不能依赖默认值,最好配置在部署模板里。

这些听起来像“管理要求”,但真到排查的时候就会发现,很多问题和这个“没规划”直接相关。比如maxmemory设得过大,导致实例常驻内存高企,一有大Key就容易触发淘汰连锁反应;又比如过期时间设计不合理,大量同批key一起过期,造成瞬间内存下降和回源风暴。

6.3 slowlog的验证作用

处理大Key时,千万别浪费慢查询日志这个工具。

redis-cli slowlog get 50

slowlog能直接告诉你哪些命令耗时高,配合scan定位到具体key。很多时候,你以为是大Key引发的问题,实际是访问方式不合理:比如没有用pipeline,批量命令一条条发送,每条都在等网络往返;或者根本没有范围查询,一次性拉全量数据。

另一个技巧:内存优化到位后,慢查询数量会立刻下降。这本身就是检验优化效果的好指标。如果一个实例的慢查询大多是hgetall或smembers,大Key嫌疑极高;如果大多是keys或flushdb,那就是命令使用习惯问题,跟数据体积无关。

7. 一些值得记住的操作习惯

真正处理过一次内存告警后,才会明白“内存淘汰策略”和“大Key处理”是同一个硬币的两面。淘汰策略决定内存满了之后“先动谁”,大Key处理决定“动了之后会不会卡死”。

我现在接手任何一个Redis实例,都会先做三件事:

  1. 拉一轮内存使用情况和maxmemory配置,确认淘汰策略是否符合业务性质。
  2. 用scan和type脚本扫描一遍大Key,处理掉体积靠前的top10。
  3. 把evicted_keys和slowlog加入监控告警,宁可误报也不能漏报。

这三件事都做完,Redis的内存问题基本就维持在“可预期”的范围内。即使活动流量突然打满内存,你也知道自己迟早会面对淘汰行为,而不是在半夜被报警声吵醒后,再去一台台机器上找大Key。

最后分享一个小细节:在测试环境,我建议直接把不同策略各跑一轮压测。把数据量、命令混合比例写进脚本,观察evicted_keys突变和请求延迟的变化。纸上谈兵的策略选择,远没有真实数据有说服力。内存治理这件事,做得越早越主动,等线上告警来了再反应,你就只能疲于奔命。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询