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 怎么验证设置是否合适
设完以后不是看一眼就完事。我会按这几步验证:
INFO memory看used_memory和maxmemory的差值,如果持续在90%以上,说明太紧,要扩或加淘汰策略。- 压测写入,观察是否触发
OOM command not allowed when used memory > 'maxmemory',如果正常业务写入就触发,说明阈值太低。 - 观察碎片率
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,按这个顺序排查:
- 执行
info memory,看used_memory离maxmemory还差多少,如果只差几百MB,说明是缓冲导致的,重点看MEMORY STATS的replication和aof字段。 - 执行
info stats,看evicted_keys涨了多少,如果正在疯狂淘汰,说明是策略在处理,而且你的淘汰速度跟不上写入速度。 - 如果想快速恢复少部分阻塞:临时
CONFIG SET maxmemory +1gb,然后慢慢优化,但不要每天加。 - 根本解法是拆分数据、加大内存规格,而不是无限调大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。
排查过程:
info memory看到used_memory 5.4GB,maxmemory 6GB,evicted_keys很高,说明淘汰非常频繁。MEMORY STATS看到overhead.hashtable.main有320MB,replication.backlog占了1MB,不算异常。redis-cli --bigkeys扫出最大的一个key是商品排行榜zset,每个value 2KB,这个key一周都没有写入,但它是volatile-lru里的无TTL key,永远不会被淘汰,白白占着350MB。- 同时发现所有缓存key的TTL都设了24小时,但写入集中在每天上午10点,晚上8点到凌晨是淘汰高峰期。
调整动作:
- 缓存策略从volatile-lru改成allkeys-lru,因为购物车和商品详情其实都可重新生成,没必要保护无TTL key。
- TTL策略从固定24小时改成24小时加随机偏移(每小时做一次随机加几分钟),避免同一时刻大批key同时过期引发缓存雪崩。
- maxmemory-samples从5调到10,淘汰精度提高。
- 后台每小时跑一次
MEMORY USAGE抽样,发现超过100MB的key单独告警,提前拆大key。 - 开启active defrag,控制碎片率在1.5以内。
调整后的效果:
- 缓存命中率回升到91%以上。
- evicted_keys降到每分钟200以下。
- DB慢查询消失。
- 用了三周后碎片率稳定在1.3左右。
这个案例给我最大的教训是:调内存也好,调淘汰策略也罢,先看清数据的访问模式再动手。把TTL随机化之后,不仅淘汰风暴解决,连数据库压力都跟着降了。
Redis内存设置的完整链路就是:搞清楚内存组成 -> 合理设置maxmemory -> 选对淘汰策略 -> 监控碎片率 -> 定期扫描大key -> 动态调整配置。这套流程走下来,Redis内存这块基本就不会再出大乱子。当然每个业务的访问模型不同,你得边做边量边调,没有银弹,但有方法论。