运营中踩过Redis的坑太多了,最典型的并不是集群故障,也不是持久化丢失,而是一个看似再简单不过的问题:Redis内存设置。上次线上告警,一台Redis实例拒绝写入,客户端疯狂报OOM command not allowed when used memory > 'maxmemory'。我连上redis-cli一看,config get maxmemory返回0,心想“默认不限制啊”,再一查maxmemory-policy,直接就沉默了——原来是noeviction,也就是说这个实例根本不是没上限,而是上限到了之后彻底摆烂,拒绝一切写入。
那次之后我把整个内存设置体系翻来覆去捋了几遍,发现这个领域没有一个参数是孤立存在的。上限、淘汰策略、过期键回收、碎片整理、监控指标,它们是一套完整的组合拳。这篇文章就把我在实际项目中验证过的东西全部分享出来,包括配置逻辑、排错思路、以及那些文档里不会明说、只能靠踩坑换来的经验。
1. 先搞明白Redis内存到底被什么占满
1.1 别只盯着key和value,还有大量“看不见”的开销
很多人说起Redis内存,第一反应是“我存了多少key,数据多大”。但真实情况远比这个复杂。Redis内存主要分三块:
- 数据本体:key和value的值,比如字符串内容、哈希里的字段、列表里的元素。
- 结构开销:存一个key,至少需要一个dictEntry结构保存指针关系,key需要独立的SDS字符串,value根据类型不同还有不同的底层数据结构。字符串走SDS,哈希如果元素少走listpack,元素多了会升级成哈希表,有序集合从listpack升级到skiplist,每一层都有内存代价。
- 运行时开销:客户端输入/输出缓冲区、AOF重写缓冲区、复制积压缓冲区(repl-backlog)、主从同步期间产生的临时内存,这些都不算进某个key的容量,但实实在在占着进程的内存。
换句话说,一个平均长度几十字节的key-value,实际占用往往比很多人印象中的“1KB”大得多。used_memory是分配器实际分配出的内存,它包含了数据、结构和部分运行时开销。而used_memory_rss是操作系统视角下进程真正占用的物理内存,它比used_memory还多一部分页表映射开销,再加上分配器因为内存碎片多分配的字节。
所以,做内存设置之前,你至少要会用一条命令看清现状:
redis-cli INFO memory重点看几个字段:
used_memory:Redis自身累计分配的内存字节数。used_memory_human:上面这个值的人性化展示。used_memory_rss:进程实际占用的物理内存,注意它可能大于used_memory。mem_fragmentation_ratio:used_memory_rss / used_memory,这是内存碎片率的直接体现。maxmemory:当前设置的内存上限,0表示不限制。maxmemory_policy:当前淘汰策略。
如果你连maxmemory都还是0,那等于Redis是在裸奔,系统物理内存多大它就能吃到多大,一旦吃满就是被操作系统OOM Killer干掉。我见过不少人报“Redis挂了我没动过配置啊”,回头一看maxmemory 0挂了好几年,挂掉根本不算意外。
1.2 64位和32位的默认差异,以及一个常见的认知误区
Redis内存上限有个著名的默认值差异:32位编译版本默认maxmemory 3GB,因为32位进程寻址空间有限,Redis会自动给你设个天花板;64位版本默认maxmemory 0,也就是不限制。
这个“不限制”本意是把内存管理交给操作系统动态调度,但在生产环境里它就是个隐患。原因很简单:Redis的淘汰机制依赖maxmemory这个边界来触发,你不设边界,它永远不触发淘汰,数据只会越来越多。就算你大部分key都设了TTL,过期键也不是立刻被物理删除的,内存依然可能被堆积的数据吃满。
我曾经接手的项目里有个实例,统计下来数据项大概只有2GB,但used_memory_rss跑到了近6GB。一看就是历史遗留角色:一年前有个业务往Redis里灌了一大批没有TTL的临时集合,任务结束后数据没清,后续业务又不断写入,淘汰策略虽然是allkeys-lru,但机器内存上限设得极高,等于形同虚设。所以别天真地以为“淘汰策略会自动帮我清理内存”,策略只是个触发器,它只在maxmemory被触及的那一瞬间才开始工作。maxmemory没设对,策略再好也白搭。
2. maxmemory上限:设多大、怎么设、设了之后还有什么在涨
2.1 别把maxmemory顶到物理内存的天花板
关于maxmemory设多大,我见过最迷的操作是:机器16GB,直接maxmemory 16gb,觉得“物理内存多大我就用多大”。这样做的结果是Redis数据内存吃满之后,RSS早就高过物理内存了,系统只能用swap,或者把进程直接杀掉。
原因在于,maxmemory限制的并不是进程的全部内存。它限制的是Redis分配器可以用于数据存储和一部分运行时结构的内存,但有几个东西其实在往“maxmemory之外”叠加:
- 内存碎片:特别是频繁增删key的场景,碎片可以让RSS远超used_memory。
- 主从复制:slave节点全量同步时要建临时RDB文件,同时维护复制缓冲区,这部分内存不受maxmemory严格约束。
- AOF重写:
BGREWRITEAOF会fork子进程,子进程写AOF文件时通过写时复制共享父进程的内存页,期间内存变化可能很剧烈。 - 客户端输出缓冲:如果一个客户端执行了
KEYS *或者大范围SMEMBERS导致返回数据量极大,输出缓冲可能瞬时吃掉数百MB。
所以,业界比较稳的做法是给maxmemory留出物理内存的缓冲。如果是主从部署,单机16GB,我通常建议maxmemory 12gb左右;如果是单机Redis且没有持久化负担,可以稍微顶一点,但也别超过物理内存的80%。另外,如果实例同时跑着RDB快照或AOF,fork子进程期间写时复制有可能让RSS短时间翻倍,这一点尤其要警惕。你可以在redis.conf里这样设置:
# redis.conf maxmemory 12gb maxmemory-policy allkeys-lru maxmemory-samples 52.2 运行时修改与持久化:config set只是“半生效”
生产环境里临时调内存是常有的事。比如某次大促前一天发现实例内存水位偏高,你想把上限赶紧拉高一点,用CONFIG SET就能立刻生效:
redis-cli CONFIG SET maxmemory 14gb但这里有个坑,CONFIG SET只改运行时配置,不改配置文件。一旦Redis重启,它又会按旧的redis.conf加载,回到老上限。你问我怎么记住要同步?有一次我就是大促调完没写回配置文件,当天没问题,下一周运维重启了几台Redis,内存上限瞬间回到原来的值,后面又有业务开始报OOM,排查了半天才发现竟然是“配置回滚”了。
正确做法是,改完之后顺手执行:
redis-cli CONFIG REWRITECONFIG REWRITE会把当前运行配置写回redis.conf。当然有个前提:Redis进程必须有写配置文件的权限,如果是容器启动时把配置挂载成只读,这个命令会报错。随手写一行CONFIG REWRITE不费事,能帮你省掉后面的很多麻烦。
2.3 云环境与容器部署下,maxmemory和容器limit的关系
如果你用Docker跑Redis,或者部署在Kubernetes里,事情又多了一层:容器本身也有内存limit。比如你给Redis容器设了--memory 8g,但Redis内部maxmemory还写着12gb,这样就很尴尬。Redis的淘汰策略根本感知不到容器limit,它只认自己统计的used_memory和maxmemory,所以它以为还能继续写入,结果容器的Cgroup直接把进程内存限制住,Redis一申请内存就会触发EGL或OOM被杀。
容器场景下比较合理的经验是:把maxmemory设成容器limit的70%到80%。例如容器limit是8GB,maxmemory就设6GB。为啥要留这么多缓冲?除了前面说的碎片和子进程写时复制之外,容器内还有底层运行时本身也会占内存。别小看这个缓冲,线上实例碎片刻度高了以后,RSS超过maxmemory一两个G是常有的事。
再补一句关于配置方式的:如果你用Redis官方镜像,大家一般通过启动参数传配置。注意redis-server --maxmemory 6gb这种写法会覆盖配置文件默认值,但不会写进conf里的文件内容,重新部署时要确保镜像或编排模板里配置一致,别只在某个测试环境手动敲了一遍。
3. 淘汰策略:八种策略,其实你只需要纠结四种
3.1 策略全家桶拆解
Redis的maxmemory-policy一共八种:noeviction、allkeys-lru、volatile-lru、allkeys-random、volatile-random、volatile-ttl、allkeys-lfu、volatile-lfu。名字看起来很复杂,关键只在于两件事:淘汰范围(是全部key还是只淘汰设了TTL的key)和淘汰依据(LRU、LFU、Random还是TTL)。
我把它们分成四个层面来理解:
| 策略 | 淘汰范围 | 淘汰依据 | 典型适用场景 |
|---|---|---|---|
| noeviction | 不淘汰 | 无 | 状态类数据,信用卡授权等不允许删除的业务 |
| allkeys-lru | 全部key | 最近最少使用 | 通用缓存,业务key大多无TTL |
| volatile-lru | 仅带TTL的key | 最近最少使用 | key基本都有过期时间,想优先淘汰冷的业务key |
| volatile-ttl | 仅带TTL的key | 剩余存活时间最短 | 定时任务类缓存,优先淘汰最快过期的 |
| allkeys-random | 全部key | 随机 | 所有key权重一致,可接受任意丢失 |
| volatile-random | 仅带TTL的key | 随机 | 权重一致且只接受淘汰有过期时间的key |
| allkeys-lfu | 全部key | 最不经常使用 | 访问频率差异极大的热点缓存场景 |
| volatile-lfu | 仅带TTL的key | 最不经常使用 | 带TTL且访问频率有梯度的业务 |
这里有一个特别常见的坑:volatile开头的策略只在“设置了过期时间”的key里做文章。如果你的业务大量key根本没设TTL,那么配置了volatile-lru或者volatile-ttl等于白配。内存满时Redis找不到任何可淘汰的过期key,最终行为会退化成noeviction——直接报OOM拒绝写入。我帮人排查过好几次类似问题,业务方的Redis内存设置里写的是maxmemory-policy volatile-ttl,但所有key都是永久留存,结果一到高峰期就开始报错。所以,选淘汰策略之前先回答一个问题:你希望Redis对你那些“永远不删除”的key下得去手吗?如果下不去手,就用noeviction配合严格的内存监控;如果能接受,就选allkeys开头的策略。
3.2 LFU不是银弹,但它确实能解决一类“访问频率差异大”的问题
很多人刚接触LFU时喜欢无脑上allkeys-lfu,觉得“频率比新鲜度更科学”。这话对一半。LFU确实适合那种少数热点key被高频访问、大量冷key偶发访问的场景,它靠记录key的访问频率来决定淘汰,冷数据很难“诈尸”把热点顶掉。传统LRU的问题在于:一个key昨天被狂刷,今天突然没人用了,它依然靠着“最近使用时间”排在前面,可能把真正在用的新key挤掉。
但LFU也有它的代价。Redis的LFU实现为每个对象在LRU字段里维护一个频率计数器,这个计数器表示的是一个近似访问次数,类似于8bit的概率计数器,存在Logistic分布或Morris Counter类似的机制——不是简单数到多少就是多少。另外LFU对新key不友好:刚上线的活动key即使马上会有大流量,初始频率也是0,极可能还没等到流量高峰就被当成冷key淘汰了。
所以我给业务配置时通常这样区分:
- 通用缓存、项目里大多数业务key没设TTL:优先
allkeys-lru,实现简单,行为稳定。 - 有明确热点且访问频率差异明显的场景(比如短视频热门榜单、商品秒杀池):可以考虑
allkeys-lfu。 - 日常业务对缓存命中率要求不高、只求内存可用:
allkeys-random最省心。
选好之后也别忘了一个配套参数:maxmemory-samples。它控制LRU/LFU采样评估的样本数,默认是5。样本数越大,淘汰决策越接近“真正的最近最少使用”,但CPU开销也越大。生产中我很少动它,保持默认5就能得到不错的近似效果。
3.3 运行时切换策略,要不要清数据
切换淘汰策略不需要清数据,它只是一个“下次内存达到上限时,按新规则执行”的行为。所以生产环境直接:
redis-cli CONFIG SET maxmemory-policy allkeys-lru redis-cli CONFIG REWRITE切换之后建议观察一段时间,重点看两个值:evicted_keys这个INFO统计字段有没有暴涨,以及写入失败率。如果evicted_keys疯涨,说明当前数据规模远大于实际所需,策略正在帮你“挤水分”,这时候要评估被淘汰的key对业务有没有影响。有一次我们给一个缓存实例从noeviction切到allkeys-lru,刚开始的半小时evicted_keys涨了上百万,但业务查询错误率没动——因为大量被淘汰的是早就在底层库里失效的旧缓存。这种“挤水分”正是我们要的效果。
4. 过期键、大key与碎片:内存设置背后容易被忽略的隐形占用
4.1 过期键不是“时间到就立刻消失”
我遇到不少人的认知是“TTL到了,key就没了,内存也该释放了”。实际上不是这样。Redis对过期键做删除,靠的是两种机制:
- 惰性删除:当访问一个key时,发现它已经过期,顺手删除并返回nil。
- 定期删除:
serverCron定时任务会抽取一部分带过期时间的key进行扫描,抽到过期的就删除。
注意“抽取”这个词,它不是全表扫描,也不是每个过期键都会在下一秒被清掉。如果一个key过期了但一直没被访问,定期任务又没扫到它,它就会留在内存里继续占地方。当这种“已经过期但还没被物理删除”的key大量堆积时,内存水位会保持在一个虚高的状态,甚至提前触碰到maxmemory,促使淘汰策略开始工作——而你原本设置的过期时间其实是想让数据自然消失,根本不想让LRU来“帮忙”。
解决思路有两个。第一,如果业务允许,设置过期时间时尽量带上随机抖动,比如set key value EX 1800可以在1800秒基础上加几秒随机值,防止大量key在同一秒集中过期,避免因触发批量删除导致主线程阻塞。第二,可以开启Redis 4.0之后的lazyfree机制,让过期键、被淘汰键和UNLINK删除都走后台线程释放:
config set lazyfree-lazy-expire yes config set lazyfree-lazy-eviction yes这两项配置很值得开启。因为删除大key或批量过期键时会同步释放内存,如果那个key是几百MB的列表或哈希,同步删除会造成主线程卡顿,后台删除能明显降低延迟毛刺。
4.2 大key和内存碎片:为什么RSS总是比maxmemory大
曾经有个集合作业的key,存了几十万个用户标签,这个key本身就把内存吃掉了快1GB。就算按淘汰策略它该被淘汰,同步删除也会让主线程卡顿好一阵,客户端看起来就是“Redis突然慢了几百毫秒”。排查的时候用MEMORY USAGE直接看单个key的开销,非常直观:
redis-cli MEMORY USAGE labels:user:10001如果返回的数字远大于业务预期,说明这个key的设计就有问题,要么拆成多个小key,要么压缩存储格式。
碎片这块也经常被忽略。mem_fragmentation_ratio如果长期大于1.5,说明分配器出现较多内存碎片,RSS看起来很吓人,可用内存又不多。为什么会产生碎片?Redis的分配器(默认jemalloc)为了性能和内存利用率,会把内存按size class划分成不同规格,你请求的是一个很小的空间,它可能给了你一个稍大的槽位;频繁增删键之后,槽位之间的空档就成了碎片。处理碎片有两条路线:
- 传统粗暴方案:主从切换,重启实例。碎片最怕重启,重启后RSS会回到真实数据大小。
- 温和方案:开启自动碎片整理。Redis 4.0之后支持
activedefrag yes,当碎片率超过阈值时会在后台搬移内存页,把碎片合并后归还给操作系统:
config set activedefrag yes config set active-defrag-threshold-lower 10 config set active-defrag-threshold-upper 100不过activedefrag不是免费的,它消耗CPU,高QPS业务慎开。我一般建议碎片率超过1.5且有实际内存压力时,考虑优先做一次主从切换或者低峰期重启,比长期让activedefrag和正常业务抢CPU要稳一些。
4.3 千万别用KEYS *查看内存大的实例
老生常谈但还是值得再说一遍:生产环境Redis凡是数据量大的,绝对不要执行KEYS *。它会阻塞主线程,遍历整个键空间,期间所有读写都会被卡住。这种“慢查询”比内存问题本身还可怕,因为它能让整台实例的服务质量瞬间崩溃。要看key分布,用redis-cli --bigkeys或自己写SCAN遍历。我习惯用redis-cli --bigkeys --i 0.1跑一轮,慢是慢一点,但能安全拿到大key列表和类型分布,为内存治理提供依据。记住,安全永远排在“方便”前面。
5. 从告警到定位:线上内存问题的完整排查链路
5.1 先把INFO memory这几行看懂
排查内存问题,离不开INFO memory。除了前面说的几个基础指标,还有几个字段容易被忽视:
mem_fragmentation_ratio:前面讲过的碎片率,小于1时通常意味着发生了swap或者分配器异常,这个状态比大于1.5还危险。used_memory_overhead:这是Redis 4.0以后拆出来的统计,表示数据之外的开销,包括过期字典、复制缓冲区、客户端缓冲区等。如果这个值占比很高,说明数据没多少,但结构开销极大,多半是大量小的key-value或大量客户端连接导致的。allocator_allocated、allocator_active、allocator_resident:这三个加起来可以看到分配器的三层视图:实际分配、占用中的内存块、映射的物理内存。它们之间的差值暗示了碎片的分布。
看这些指标,我最在意的是used_memory_rss和maxmemory的关系。如果RSS长期压在物理内存线上,但used_memory还没到maxmemory,问题基本可以锁定为碎片/子进程/客户端缓冲这类“看不见”的内存。反之,如果used_memory已经越过maxmemory线,说明数据本身确实超了,该考虑加容量或治理数据。
5.2 一个真实的内存上涨排查过程
我接下来说一个比较有代表性的场景:某实例内存平稳很久后突然开始每天涨几百MB,大约一周后必达maxmemory。排查链路是这样走的:
- 第一件事,
INFO memory看输出来源:发现used_memory上涨的同时expired_keys也在涨,但每秒回收量远跟不上新增量。这说明有很多带TTL的key堆积,但还没被定期过期任务清掉。 - 用
redis-cli --bigkeys看一眼key分布,发现大量string类型的key,前缀为counter:user:,数量在上百万级别。 - 进一步查业务代码,发现这些
counter:user:*的key用于记录用户每日计数,逻辑上应该在第二天失效,但因为并发写入时没有带上EXPIRE,只有第一次创建key时才设了TTL,后来的重复写入把TTL冲掉了。 - 修复方案:写入逻辑统一用Lua脚本,确保每次写入都同时刷新过期时间;已有的存量key通过
SCAN配合EXPIRE批量补设TTL。 - 调整maxmemory-policy,确认为
allkeys-lru,就算未来有漏设TTL的key,也能通过淘汰保护整体可用性。
这个案例里最值得注意的一点是:一开始看着像“maxmemory设置不对”,但本质是TTL设置被业务代码破坏了。内存设置本身不背这个锅,但不通过完整排查你也很难发现真正的病灶。
5.3 日常监控建议
内存问题讲究“治未病”。我通常建议至少盯四个指标,配四个告警阈值:
used_memory / maxmemory比例超过70%:预警,提前排查是否有大key或过期键堆积。mem_fragmentation_ratio大于1.5且持续半小时:考虑碎片治理。evicted_keys持续上涨:关注淘汰是否在大量发生,结合业务判断是否容量不足。maxmemory未配置或设为0的实例:这本身就该报警,说明处于“无管控”状态。
如果你的监控体系是通过redis-cli INFO memory采集的,写个简单的定时脚本就能拿到数据。比如每天记录一次used_memory_human和maxmemory_human到日志,对比一周趋势,基本能提前感知大多数内存异常。
6. 不同场景下的内存设置经验
6.1 缓存型业务和计数型业务,配置是两套玩法
缓存型业务(比如接口响应缓存、商品详情缓存)的key基本都能重建,这类业务适合把重点放在“命中率”上。推荐maxmemory-policy allkeys-lru或者allkeys-lfu,key尽量不设长TTL,靠淘汰机制自动挤出冷数据,让Redis保持在一个“永远在最有价值的数据上工作”的状态。
计数型业务则是另一类。你拿Redis做点赞数、访问量、登录次数,这些数据一旦丢了就是事故,绝不能为了“腾内存”被LRU随便淘汰。这类业务我强烈建议设置maxmemory-policy noeviction,同时精确控制每个计数key的TTL和过期时间,并且给实例单独划一个比较宽裕的maxmemory。为什么宽裕?因为计数key通常不会自动缩小,你只能靠过期策略定期清理。所以计数型业务往往需要“内存跑得慢一点”甚至“预留半年增长空间”。我给计数型实例设maxmemory时,倾向按当前每日递增量的30到50倍预留,避免三个月后回来救火。
6.2 key设计本身就在影响内存
内存设置做得再好,也救不了无节制的key设计。我见过一个业务把用户实时数据的每个字段都拆成一个独立key,比如userinfo:10001:name、userinfo:10001:age,光一个人就有几十个key。最终这个实例因为海量小key,used_memory_overhead直接占了总内存的40%以上。每个key的dictEntry、SDS、过期时间戳在内存里都是实打实的开销,虽然单个只有几十字节,架不住数量大啊。
解决办法是优先使用哈希结构合并同一业务实体的字段。同样存100万个用户的四个字段,哈希方案可以在一个key内存多张哈希表,整体结构开销远低于百万级独立key。这类经验看似和“内存设置”没有关系,但实际上内存设置的边界条件全是由数据模型撑起来的。数据模型越烂,maxmemory涨得越快,淘汰策略越难选。
6.3 常见误区清单
最后列一份我自己踩过或看别人踩过的误区,当作对照检查:
- 误区一:maxmemory设成0,觉得“不限制就是好事”。实际上无上限=无淘汰=无保护,系统OOM会直接杀掉进程。
- 误区二:全库key都没TTL,却配了volatile-lru/volatile-ttl。这等于没配淘汰策略,内存满了直接拒绝写入。
- 误区三:maxmemory直接顶满物理内存。RSS因为碎片、子进程、客户端缓冲区等原因超出上限时,机器会触发swap或OOM。
- 误区四:只调
CONFIG SET不执行CONFIG REWRITE,Redis重启后配置回滚。 - 误区五:aof或RDB开启时,忽略fork子进程内存翻倍的可能,导致机器内存不够。
- 误区六:开启activedefrag后不管CPU负载,高QPS高峰期直接把内存整理跑成CPU瓶颈。
写在最后的一点心得
Redis内存设置不是“配两个参数就完事”的静态操作,它更像一个持续校准的过程。我自己的习惯是每次调整完maxmemory、淘汰策略或者碎片相关参数之后,把当时的INFO memory输出、QPS和机器free信息存一份快照,隔一周再看一眼趋势。这样一来,配置到底改善了什么、有没有副作用,心里都有数。
如果非要对刚接触这块的读者给一句建议,我会说:先把默认的0和noeviction改掉,给Redis画一条明确的内存红线,再根据业务模式选淘汰策略。这个起手式,能让绝大多数内存问题在一开始就被挡住。后面那些精细调节,都是在这条红线上添砖加瓦。