☰
Redis内存管理实战:maxmemory配置与淘汰策略深度解析
2026/10/14 6:19:50 网站建设 项目流程

1. 先把Redis的内存账算清楚

Redis吃内存这件事,很多人的第一反应是"那就调maxmemory呗",但如果你不知道内存到底被谁吃了,那调参基本就是瞎调。我见过好几个项目,maxmemory设了512MB,结果Redis 4G的物理内存全被占满,应用直接OOM,一查发现是AOF重写和主从同步的缓冲区在悄悄吃内存。所以第一步不是配置maxmemory,而是搞清楚Redis内存从哪来、到哪去。

Redis的内存占用,按大头排下来主要是这几块:

  • 键值对数据本身:这是你存进去的字符串、hash、list、set、zset,每个对象都有头部结构、指针、元数据,Redis的dictEntry、robj、sds这些结构都有固定开销。
  • 过期键与淘汰辅助结构:每个key的过期时间会记录在expires字典里,LRU/LFU信息也存在robj中,这些都要占内存。
  • 主从复制缓存:全量同步时,主节点要生成RDB并缓存在输出缓冲区,增量同步还有replication backlog,默认1MB起步,可以用参数调大。
  • AOF缓冲与重写缓冲:AOF追加时数据先写buf,重写时还会把期间的新写操作缓冲到aof_rewrite_buf。
  • 客户端缓冲区:每个连接的输入输出缓冲,尤其是订阅、慢查询、大key返回,输出缓冲会膨胀得很夸张。
  • 内存碎片:jemalloc分配器的碎片率一般在1.01到1.5之间,业务波动大时可能到2以上,这算"隐性开销"。

很多人的误区是"Redis内存 = 数据量 = keyspace里的字节数",实际完全不是这样。举个例子,一个只有三个字段的hash,直接存字符串拼接可能只有60字节,但用hash结构存,ziplist/listpack还能压到80字节左右,一旦超过配置阈值升级到hashtable,开销瞬间翻倍。所以"内存设置"这四个字,核心是先把总量想清楚,再谈配置和策略。

我在实际项目中喜欢先用redis-cli --memstat-stats扫一遍现状,看看哪些key类型在吃内存,再对着info memory看used_memory和used_memory_rss差值,如果差值超过20%,优先处理碎片而不是调maxmemory。顺序搞反了,你后面怎么调都别想稳定。

2. maxmemory:设多少、怎么设、设完怎么验证

2.1 maxmemory的本质不是"限制",是"保护"

先纠正一个常见误解:maxmemory不是为了让Redis少占内存,而是为了在内存达到阈值时触发自我保护机制——要么拒绝写入(noeviction),要么按策略淘汰旧key。如果maxmemory设成0,相当于不限制,Redis理论上可以用光整台机器的内存,这在业务膨胀时是灾难。

我从经验上给个参考公式:

合理maxmemory = (总内存 - 系统预留 - 其他进程占用) × 0.7到0.8

之所以留20%到30%余量,是为了给AOF重写、主从全量同步、客户端输出缓冲这类突发内存留空间。你想想,如果maxmemory设到物理内存的95%,一旦触发BGSAVE或者主从重连,内存直接爆掉,Redis会被系统OOM killer杀掉,这不是淘汰数据的问题,是整个实例没了。

2.2 动态设置与持久化

Redis的maxmemory支持运行时动态调整,这比重启改配置好用得多:

# 运行时设置,立即生效 127.0.0.1:6379> CONFIG SET maxmemory 4gb OK # 确认生效 127.0.0.1:6379> CONFIG GET maxmemory 1) "maxmemory" 2) "4294967296" # 需要重启也生效,必须同时写进redis.conf # 在redis.conf里设置: # maxmemory 4gb

这里有个坑:CONFIG SET改完如果没写进redis.conf,Redis重启后又会回到旧值。高可用场景下,主节点动态改了,从节点并不会同步这个配置,如果主从的内存规格不一样,你要记得在每台机器上单独调。我在生产环境一般用配置管理工具统一分发,改完发到所有节点再滚动重启,避免配置漂移。

还有一个细节:maxmemory的单位可以带后缀,1gb、512mb、100kb都会自动换算成字节,注意gb和gb大小写其实不敏感,但建议统一小写。设置完后务必用config rewrite把当前配置写回配置文件,否则下次启动就丢了。

2.3 怎么验证设置是否合适

设完以后不是看一眼就完事。我会按这几步验证:

  1. INFO memory看used_memory和maxmemory的差值,如果持续在90%以上,说明太紧,要扩或加淘汰策略。
  2. 压测写入,观察是否触发OOM command not allowed when used memory > 'maxmemory',如果正常业务写入就触发,说明阈值太低。
  3. 观察碎片率used_memory_rss / used_memory,如果设置后碎片率涨到2以上,说明频繁淘汰导致分配器频繁分配释放,可能需要配合active defrag。

我踩过一个具体的坑:某活动页缓存,maxmemory从2gb调到1gb,想着缓存数据量没那么大,结果活动页一上量,Redis直接进入淘汰风暴(eviction storm),大量key被LRU干掉,后端缓存命中率骤降,数据库被拖垮。后来我加了一层min-recently-used-frequency概念:不对——直接说结论:下调maxmemory要谨慎,宁可多留余量,也不要压到接近used_memory。KEY的淘汰是有代价的,淘汰风暴比内存超限更可怕。

3. 内存淘汰策略:不是设个LRU就完事

3.1 8种淘汰策略先过一遍

Redis的maxmemory-policy有8个可选值,我直接按用途给你拆开:

策略名行为适用场景
noeviction不淘汰,写报OOM错误数据不可丢,用Redis做可靠的队列/存储
allkeys-lru所有key按LRU淘汰纯缓存,无持久化依赖
volatile-lru只淘汰设置了过期时间的key混合场景,希望长期不失效的key尽量保留
allkeys-random随机淘汰所有key缓存,key访问频率接近
volatile-random随机淘汰有过期时间的key少用
volatile-ttl淘汰剩余TTL最短的key少用,坑多
allkeys-lfu所有key按LFU淘汰访问模式倾斜明显的缓存
volatile-lfu只淘汰有过期时间的key按LFU混合场景

这里要直说一点:官方文档和网上教程都爱推荐volatile-ttl,我的实际经验是它性能差、坑多。Redis实现volatile-ttl需要遍历所有带TTL的key去取最小剩余时间,每次淘汰都是一次O(N)扫描,在key量大的时候对CPU是白白消耗。相比之下,LRU和LFU都是近似算法,直接从采样池里选,成本低得多。

3.2 LRU是不是真的"最近最少使用"

Redis的LRU不是严格LRU,它走的是近似LRU:给每个key维护一个24bit的lru时钟(秒级精度),淘汰时随机采样N个key,从这N个key里挑最久没访问的那个淘汰。默认采样数是5,对应配置maxmemory-samples。

这个采样数的影响比很多人想象的大:

# 默认5,淘汰精度一般,CPU开销小 maxmemory-samples 5 # 调大后淘汰更精准,但每次淘汰更多CPU开销 maxmemory-samples 10 # 接近精确LRU,但淘汰时CPU明显上升 maxmemory-samples 20

我建议在Redis 6.x以上版本可以保持默认5,因为还有个熔断机制:采样后淘汰的key不一定是全局最久未用,但通过增加采样数能大幅提升精度。关键是别频繁触发淘汰,一旦发现evicted_keys快速增长,优先考虑扩大maxmemory或者拆分实例,而不是无限调大采样数。

3.3 LFU比LRU更适合"热点集中"的业务

LFU全称Least Frequently Used,Redis 3.0引入,4.0以后稳定。它统计每个key的访问频次,频次低的优先淘汰,配合对数增长和衰减机制,比LRU更能防止"偶发热点key永久驻留"的问题。LFU有两个参数:

  • lfu-log-factor:控制频次增长曲线,默认10,越大增长越慢。
  • lfu-decay-time:控制频次衰减(按分钟),默认1,1分钟没访问频次减半。

如果业务是典型的二八分布(20%的key承载80%的访问),LFU会比LRU明显减少缓存穿透。但注意LFU有状态存储成本,每个key要多占内存,redisObject里那24bit的lru字段改用lfu+时间戳混合编码。对键量极大的场景会带来少量内存上涨,我用过之后测下来大约多2%到4%。

3.4 线上怎么选淘汰策略

我的选型逻辑很简单:

  • 纯缓存、数据丢了能重算:allkeys-lru,这是最省心的。
  • 有些key必须常驻(比如配置类),但缓存主体可淘汰:volatile-lru,前提是所有可淘汰的key都显式设置了过期时间。
  • 缓存访问热点集中:allkeys-lfu,尤其是推荐排序、商品热度这类场景。
  • 数据不能丢、写入是主要负载:noeviction,宁可报错也别丢数据,让上层应用去重试或降级。

特别吐槽一个场景:有人把session存Redis又设了TTL,看到volatile-lru觉得完美,结果忘了有个别session key没设过期时间,这些永不淘汰的key积少成多,最后used_memory接近maxmemory,淘汰风暴来了全在淘汰有过期时间的key,那些没设TTL的常驻key把内存占死。这个坑我帮人排查过不止一次,用volatile系列策略之前,先确认所有可淘汰key都设置过TTL。

4. 内存碎片与主动整理:很多人忽略的"内存黑洞"

4.1 碎片率怎么读

Redis基于jemalloc分配内存,jemalloc按大小分级,你的key长度跟分配档位不匹配时,就会产生碎片。看两个指标:

  • used_memory:实际数据占用的字节数,不含碎片。
  • used_memory_rss:进程向操作系统申请的内存,包括碎片。

碎片率 = used_memory_rss / used_memory,具体判断:

碎片率含义处理方法
1 ~ 1.5正常范围无需处理
1.5 ~ 2偏高,有优化空间考虑activedefrag或重启
2以上严重碎片化优先处理,且检查是否频繁淘汰
小于1说明内存被swap到磁盘了立即查swap占用,这是性能灾难

4.2 active defrag的开启与参数

Redis 4.0以上支持在线碎片整理,开启方式是:

CONFIG SET activedefrag yes CONFIG SET active-defrag-ignore-bytes 100mb CONFIG SET active-defrag-threshold-lower 10 CONFIG SET active-defrag-threshold-upper 100 CONFIG SET active-defrag-cycle-min 5 CONFIG SET active-defrag-cycle-max 75

这几个参数的含义:

  • active-defrag-ignore-bytes:碎片小于这个值不整理,太小了没意义。
  • active-defrag-threshold-lower:碎片率超过10%开始整理(用百分比表示,这里是10%)。
  • active-defrag-threshold-upper:碎片率超过100%时,全力整理。
  • active-defrag-cycle-min/max:每个周期给整理工作分配多少CPU时间,5到75,意味着CPU闲的时候多干点,忙的时候少干点。

我实测的效果:某实例碎片率1.8,开启active defrag后一周内降到1.3左右,对线上读写几乎无感。但注意整理过程有内存拷贝开销,如果你的实例是64GB以上而且CPU长期跑满,建议在低峰期开,并盯一下INFO defrag里的defrag_malloc_total是不是在涨。

4.3 重启是最土但最有效的碎骨折叠

Active defrag不能处理的极端情况——比如碎片率3以上,且实例启动时间很长、key大小差异极大——重启一次Redis可能直接把碎片率降到1.1。成本看业务:

  • 主从架构下可以先切换从节点,重启主节点再切回,几乎无感。
  • 单机实例要考虑缓存预热成本,但如果是缓存且能容忍短暂miss,重启是划算的。

我处理过一个典型案例:某系统AOF持久化开着,从没重启过,运行200天后used_memory 6GB,used_memory_rss 15GB,碎片率2.48。active defrag跑了一周效果不明显,因为碎片分布在大块zone里。后来在低峰期做了主从切换 + 重启,RSS直接降到6.5GB,物理内存占用降了接近60%。

所以内存设置这件事,不只是配置参数,是配置 + 日常观测 + 周期性运维动作的组合拳。

5. 命令行武器库:怎么优雅地看内存和调内存

5.1 INFO memory 字段逐个解读

每次线上排查,我都会先执行info memory,这是最快的体检入口。关键字段:

  • used_memory:Redis分配器分配的内存,包含数据本身。
  • used_memory_human:可读格式,方便快速了解。
  • used_memory_rss:进程常驻内存,含碎片和分配器额外占用。
  • used_memory_peak/used_memory_peak_human:历史峰值,很有参考价值,如果峰值远高于当前值,说明之前有过突增内存的操作(批量写入、AOF重写)。
  • used_memory_lua/used_memory_scripts_cache:Lua脚本占用的内存,脚本写多了也是个隐藏占用。
  • maxmemory:设置的最大内存。
  • maxmemory_policy:当前淘汰策略。
  • mem_fragmentation_ratio:碎片率,前面说过了。
  • mem_allocator:分配器类型,一般是jemalloc。

5.2 MEMORY 子命令:定位大key和具体内存

Redis 4.0后提供memory命令家族,拿来排查太好用了:

# 查看一个key的具体内存占用 127.0.0.1:6379> MEMORY USAGE product:detail:123456 (integer) 2048 # 查看整个实例内存统计 127.0.0.1:6379> MEMORY STATS 1) "peak.allocated" 2) (integer) 8241804864 3) "total.allocated" 4) (integer) 6098767872 5) "startup.allocated" 6) (integer) 857478784 7) "replication.backlog" 8) (integer) 1048576 9) "clients.slaves" 10) (integer) 0 11) "clients.normal" 12) (integer) 466560 13) "aof.buffer" 14) (integer) 0 15) "db.0" 16) (integer) 8586681344 17) "overhead.hashtable.main" 18) (integer) 41437698 19) "overhead.hashtable.expires" 20) (integer) 133431

这些字段都是宝藏。比如replication.backlog就是主从复制缓冲,clients.slaves是从节点输出缓冲,如果这两个数值异常大,说明同步出问题了。overhead.hashtable.main和overhead.hashtable.expires是主字典和过期字典的底层结构开销,key数量多时这里也会吃掉几百MB。

我见过一次线上实例RSS降到maxmemory的80%还触发OOM,排查发现replication.backlog被调成512MB(默认1MB),同时挂了一个长时间落后的从节点,一个从节点的输出缓冲就吃了3GB。每次加从节点前,一定先看MEMORY STATS,特别是clients.slaves那段。

5.3 用bigkeys和memstat扫雷

# 扫描大key,注意这是全量scan,生产低峰期用 redis-cli --bigkeys --host 192.168.1.10 --port 6379 # 汇总各类型内存占用 redis-cli --memstat-stats --host 192.168.1.10 --port 6379

--bigkeys扫完会列出每个类型最大的几个key,比如"Biggest string found"、"Biggest list found"。--memstat-stats会按类型聚合内存占用,能快速判断是字符串吃内存还是hash吃内存。这两个工具建议接入定期巡检,比如每周跑一次,把结果输出到日志,发现大key趋势线上涨就提前干预。

5.4 监控指标建议

生产环境我至少在监控面板上放这几个指标:

  • used_memory与maxmemory的比例,超过80%触发告警。
  • mem_fragmentation_ratio,超过1.8触发告警。
  • evicted_keys速率,超过每秒100个触发告警。
  • rejected_connections、total_error_replies,这两个跟maxmemory过小直接相关。
  • used_memory_peak增长趋势,判断业务是不是在涨。

6. 常见问题排查实录

6.1 写入报错:OOM command not allowed when used memory > 'maxmemory'

这是最经典的问题。遇到它先别直接调大maxmemory,按这个顺序排查:

  1. 执行info memory,看used_memory离maxmemory还差多少,如果只差几百MB,说明是缓冲导致的,重点看MEMORY STATS的replication和aof字段。
  2. 执行info stats,看evicted_keys涨了多少,如果正在疯狂淘汰,说明是策略在处理,而且你的淘汰速度跟不上写入速度。
  3. 如果想快速恢复少部分阻塞:临时CONFIG SET maxmemory +1gb,然后慢慢优化,但不要每天加。
  4. 根本解法是拆分数据、加大内存规格,而不是无限调大maxmemory。

6.2 设置了allkeys-lru,但还是全被淘汰了

常见原因有三层:

  • key没有过期时间也照常被淘汰:这是allkeys-lru的正常行为,全字典参与淘汰,不代表能保护谁。
  • 淘汰方向反了:如果你用的是volatile-lru,而某些key没设TTL,它们永远不参与淘汰,导致实际可淘汰范围极窄,数据疯狂被淘汰也没用。
  • 采样数太小导致淘汰错误命中:maxmemory-samples=5时,淘汰引擎可能选到刚被访问的key,导致缓存命中率下降。改成10试三天,看hit_rate有没有提升。

6.3 内存碎片率1.0以下,发生了什么?

碎片率小于1,意味着used_memory_rss小于used_memory,这在正常进程几乎不可能。常见解释是swap把部分内存换出去了,检查/proc/pid/smaps或者free -h的swap使用量。如果swap不为零,说明物理内存吃紧,不只是Redis的问题,整个系统的内存水位都要检查。处理方式是加物理内存或减少实例规格,同时设置vm.swappiness为1或0,让系统优先回收文件缓存而不是swap进程内存。

6.4 BGSAVE/AOF重写后内存飙升

AOF重写是fork子进程的过程,采用copy-on-write机制,写操作发生时父子进程各自持有一份内存页,这会让RSS瞬间翻倍。如果maxmemory按物理内存的95%设置,一次BGSAVE直接顶爆。我的经验是:

  • 物理内存64GB,单实例Redis,建议maxmemory设48GB(75%)以下。
  • 物理内存48GB,如果还开了AOF做持久化,建议上限32GB。
  • 打开aof-rewrite-incremental-fsync让重写时边写边刷盘,降低瞬时内存。
  • 如果重写频率高(比如每秒写入太大),考虑关掉AOF只留RDB,或者用外部组件做持久化。

6.5 主从同步时从节点连接断断续续

如果MEMORY STATS里clients.slaves值持续高位,压满输出缓冲,从节点会不断断连重连,陷入全量同步循环。可以调大client-output-buffer-limit中的slave部分:

# redis.conf或config set client-output-buffer-limit slave 1gb 256mb 60

但更本质的做法是检查从节点到主节点的网络质量、是否因为主节点内存紧张产生淘汰风暴、或者主节点是否频繁执行耗时命令导致同步延迟。调buffer只是给问题贴创可贴,不是根治,但应急时确实能救火。

6.6 排查步骤总结

遇到Redis内存类问题,我建议按"三查一看"来做:

一查info memory看总量和碎片; 二查MEMORY STATS看缓冲区占用; 三查slowlog和monitor看是否有大key读取导致缓冲膨胀; 最后一看,直接redis-cli --bigkeys --memstat-stats扫一眼全局。

按这个顺序排查,绝大多数内存问题十分钟内能定位。

7. 一段实际调优记录,给你做个参考

最后分享一个典型的调优过程,来自我接触过的一个电商后台缓存系统。实例配置是8GB内存,缓存商品详情和用户购物车,QPS峰值约3万,数据量大概60GB,依赖Redis缓存扛住数据库压力。

初始状态:maxmemory=6GB,policy=volatile-lru,maxmemory-samples=5,碎片率1.42。 症状:缓存命中率从92%掉到80%,evicted_keys每分钟超过5000,DB压力翻倍,出现慢SQL。

排查过程:

  1. info memory看到used_memory 5.4GB,maxmemory 6GB,evicted_keys很高,说明淘汰非常频繁。
  2. MEMORY STATS看到overhead.hashtable.main有320MB,replication.backlog占了1MB,不算异常。
  3. redis-cli --bigkeys扫出最大的一个key是商品排行榜zset,每个value 2KB,这个key一周都没有写入,但它是volatile-lru里的无TTL key,永远不会被淘汰,白白占着350MB。
  4. 同时发现所有缓存key的TTL都设了24小时,但写入集中在每天上午10点,晚上8点到凌晨是淘汰高峰期。

调整动作:

  1. 缓存策略从volatile-lru改成allkeys-lru,因为购物车和商品详情其实都可重新生成,没必要保护无TTL key。
  2. TTL策略从固定24小时改成24小时加随机偏移(每小时做一次随机加几分钟),避免同一时刻大批key同时过期引发缓存雪崩。
  3. maxmemory-samples从5调到10,淘汰精度提高。
  4. 后台每小时跑一次MEMORY USAGE抽样,发现超过100MB的key单独告警,提前拆大key。
  5. 开启active defrag,控制碎片率在1.5以内。

调整后的效果:

  • 缓存命中率回升到91%以上。
  • evicted_keys降到每分钟200以下。
  • DB慢查询消失。
  • 用了三周后碎片率稳定在1.3左右。

这个案例给我最大的教训是:调内存也好,调淘汰策略也罢,先看清数据的访问模式再动手。把TTL随机化之后,不仅淘汰风暴解决,连数据库压力都跟着降了。

Redis内存设置的完整链路就是:搞清楚内存组成 -> 合理设置maxmemory -> 选对淘汰策略 -> 监控碎片率 -> 定期扫描大key -> 动态调整配置。这套流程走下来,Redis内存这块基本就不会再出大乱子。当然每个业务的访问模型不同,你得边做边量边调,没有银弹,但有方法论。

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

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

立即咨询