ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis maxmemory深度解析:内存超限、淘汰策略与缓存治理实战

Redis maxmemory深度解析:内存超限、淘汰策略与缓存治理实战 Redis 这个maxmemory的坑我估计十个人里有八个都踩过。明明设置了最大内存 2GB结果 Redis 内存都飙到 2.6GB 了还在继续写入你想让它拒写它偏不你想让它淘汰 key它给你 OOM 报错。最后服务直接卡死缓存雪崩数据库被打穿。我自己在维护生产环境的时候这个问题前前后后折腾了好几轮今天就把这块硬骨头从头到尾掰开揉碎讲清楚。这篇文章适合谁看不管是刚把 Redis 当缓存用的小白还是已经在维护 Redis 集群、做缓存治理的运维开发都应该认真读一遍。我会先把 maxmemory 的本质讲透然后给出完整的配置方案、淘汰策略选型逻辑、排查命令、以及我踩过的好几个坑。你照着操作至少能保证 Redis 在内存超限时不会“失控”。1. 先搞清楚 maxmemory 到底卡在哪里1.1 你以为设置了 maxmemoryRedis 就会自动拒写其实不是很多人的第一反应是maxmemory不就是 Redis 的内存上限吗到了这个值就该拒写新 key 了这个理解只对了一半甚至在某些场景下完全不对。maxmemory的全称是maxmemory bytes它设置的是 Redis数据内存的上限。但关键在于Redis 达到上限后的动作完全取决于maxmemory-policy这个配置。默认情况下maxmemory-policy的值是noeviction也就是说内存满了之后不淘汰任何 key但拒绝新的写入请求同时给客户端返回 OOM 错误。这就带来两个非常让人的困惑的现象第一Redis 实际进程占用的内存RSS远大于 maxmemory。因为 maxmemory 只统计数据部分像客户端缓冲区、复制积压缓冲区、AOF 重写缓冲区、主从同步的 backlog这些内存都是不被 maxmemory 限制的。这就是为什么你设置了 2GB结果 RSS 到 2.6GB 也不稀奇。第二noeviction 策略下写了 OOM 错误但数据还在增长。因为如果你有一个特别大的 key 在持续追加比如APPEND一个大 list或者SETRANGERedis 会先把数据写进去然后才检查是否超限。这个大 key 本身就能把内存顶破。所以别把maxmemory当成一个“硬性闸门”它更像是一个“水位线”到达水位之后具体怎么处理要看你装了什么策略。1.2 Redis 数据类型决定了内存增长方式回到热搜词里的“redis 数据类型”这块跟内存超限其实关系密切。很多人只盯着一个 key 的 value 大小忽略了 Redis 内部对数据结构的编码优化。比如同一个 list元素少的时候用quicklist元素多了会转成listpack或 linked list一个 string 超过 512MB 是上限但一个 set 可以存上亿个元素。内存增长不是线性的跟编码方式、元素数量、value 大小都有关系。从内存治理角度我习惯把 Redis 的内存占用拆成三块数据内存就是 key 和 value 本身占用的内存。元数据内存每个 key 都会有一个 dictEntry、key 对象、value 对象、过期时间等辅助结构。光一个空 key 就可能占 80~100 字节。运行时内存命令缓冲区、主从复制、AOF 缓冲、lua 脚本缓存等。如果你在做内存治理光看INFO memory里的used_memory是远远不够的你得拆开看used_memory_rss、used_memory_dataset、used_memory_overhead。这几个指标能直接暴露内存黑洞到底在哪。2. 核心配置与解决步骤从设置到策略一整套2.1 配置 maxmemory 的正确姿势修改maxmemory有两种方式我建议你根据场景选方式一修改配置文件 redis.conf# 单位是字节 maxmemory 2gb如果你是 4.0 以上的 Redis还可以用带单位的形式看着直观maxmemory 2gb方式二运行时动态修改生产环境最推荐# 设置最大内存 2GB 127.0.0.1:6379 CONFIG SET maxmemory 2gb # 设置淘汰策略为 allkeys-lru 127.0.0.1:6379 CONFIG SET maxmemory-policy allkeys-lru # 确认是否生效 127.0.0.1:6379 CONFIG GET maxmemory这里有个非常关键的细节如果当前 Redis 的实际内存已经超过了 maxmemory你再通过 CONFIG SET 去设置一个更小的 maxmemory会导致 Redis 立即开始尝试淘汰 key直到内存低于新限制。这个过程如果 key 太多太密集会阻塞主线程造成命令超时。这就是为什么热搜词里会有一个“redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException”——很多公司用 Lettuce 客户端一旦发生淘汰风暴Redis 主线程卡住Lettuce 直接超时。注意所以线上改 maxmemory 的时候别一次性砍太狠。比如现在实际内存 3GB你想改成 1GB建议分两步先改到 2GB等淘汰稳定了再改到 1GB。不然生产事故可能就是一瞬间的事。2.2 八种淘汰策略到底怎么选——这是解决问题的核心https://redis.io/docs/latest/commands/ 上文档对maxmemory-policy的解释很清晰但很多人看了还是不知道怎么选。我直接用大白话翻译一遍策略作用范围具体行为适用场景noeviction全局不淘汰拒绝写入严格不允许丢 key 的场景比如分布式锁、消息队列allkeys-lru所有 key按最近最少使用淘汰纯缓存场景最常用volatile-lru设置了过期时间的 key按最近最少使用淘汰一部分是永久 key一部分是缓存 keyallkeys-random所有 key随机淘汰淘汰分布均匀的场景volatile-random设置了过期时间的 key随机淘汰同上有 TTL 的 key 才淘汰volatile-ttl设置了过期时间的 key优先淘汰剩余 TTL 短的有明确过期时间的会话类数据allkeys-lfu所有 key按访问频率淘汰需要精准识别热数据的缓存场景Redis 4.0volatile-lfu设置了过期时间的 key按访问频率淘汰同上有 TTL 的 key 才淘汰我在生产环境里最常用的两个组合是纯缓存场景maxmemory-policy allkeys-lru。这种场景下Redis 里的数据都是缓存丢了可以从数据库重新加载所以让 Redis 自己按 LRU 淘汰最合理。混合场景缓存 业务数据volatile-lru或volatile-lfu。这样只淘汰带过期时间的缓存 key永久 key比如配置数据、token 黑名单不参与淘汰安全。2.3 LRU 和 LFU 应该选哪个这也是一个高频问题。Redis 里的 LRU 不是“严格 LRU”而是近似 LRU。Redis 采样的方式内存达到上限的时候从哈希表中随机抽取一部分 key默认采样 5 个maxmemory-samples 5然后淘汰其中最久没被访问的那个。所以“近似”就导致一个问题如果数据访问满足“突发热点”模式LRU 会误杀一些其实很热的 key。举个例子一个 key 今天被疯狂访问 1 万次然后突然安静了 10 分钟如果是 LRU这 10 分钟它的“最近访问时间”就落后了很可能被淘汰但可能明天它又是一波热点。LFU 就不一样它通过访问频次计数来规避这个问题。Redis 4.0 才引入 LFU本质上是给每个 key 维护一个访问频次计数器频次低的最先淘汰。我维护过一个电商秒杀系统热 key 忽高忽低后来换成allkeys-lfu之后缓存命中率提升了接近 8 个百分点。不过你要注意LFU 有“频次老化”问题一个 key 曾经很热现在不热了它的计数器依然很高导致它霸占内存却没什么用。Redis 对这个问题做了折中处理通过衰减因子来控制计数器的衰减速度。提示如果你确实不知道选什么无脑allkeys-lru不会出大错但如果你对数据分析有要求试试allkeys-lfu效果往往更好。3. 实操从现象到解决的全流程复盘3.1 排查内存超限的完整命令序列我拿到一个 Redis 内存超限问题第一件事不是改配置而是先看现象。用下面这套命令五分钟内能定位大概方向# 1. 看内存概况 127.0.0.1:6379 INFO memory # 2. 看键的数量和类型分布 127.0.0.1:6379 DBSIZE 127.0.0.1:6379 INFO keyspace # 3. 看哪些 key 占内存最大 # 这里可以用 redis-cli 内置的 --bigkeys 扫描 redis-cli --bigkeys # 4. 看数据库有没有设置了 TTL 的 key避免大量永久 key 堆积 127.0.0.1:6379 INFO statsINFO memory里的几个关键字段我解释一下used_memory由 Redis 分配器分配的总内存可以理解为“有效数据内存”。used_memory_rss从操作系统角度看Redis 进程占用的物理内存。used_memory_dataset数据实际占用的内存不含元数据开销。used_memory_overhead数据之外的开销包括 dictEntry、key 对象、过期键管理等这个数值如果很大说明 key 非常多。我见过一个案例用户抱怨 Redis 内存高到快爆了结果一看used_memory_dataset只有 200MB但used_memory_overhead有 1.2GB。原因是这个业务方在 Redis 里存了3000 多万个 key每个 key 都很短value 也很短典型的“大量小 key 吃垮 Redis”。这种情况你优化单个 key 没用得从 key 设计上做合并比如 hash 结构代替 string 前缀拆分。3.2 redis-cli --bigkeys 的实际用法与陷阱redis-cli --bigkeys是我每次排查内存问题的第一步它本质上是一个内置的扫描工具会遍历所有 key然后按照 string、list、set、hash、zset 分类统计出每一类中最大的 key。# 最常用法默认扫描 100 万个 key redis-cli --bigkeys # 如果想限制扫描数量用 -i 控制每扫 100 个 key 停 0.1 秒避免阻塞主线程 redis-cli --bigkeys -i 0.1 # 指定主机端口 redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1这在生产环境是个危险操作因为--bigkeys本质上是执行SCAN命令然后对每个 key 执行STRLEN、LLEN、HLEN等命令。如果你是 6.0 以下的 Redis它会以KEYS的方式遍历所有 key这在生产环境极其危险可能直接阻塞 Redis 主线程几十秒。注意所以在线环境千万别直接裸跑redis-cli --bigkeys一定要加-i 0.1甚至-i 0.5来降低扫描频率。另外如果你用的是 Redis 6.2可以用redis-cli --bigkeys自带的 batch 模式它会用SCAN分批处理相对安全。3.3 最关键的步骤找到那些“又大又没 TTL”的 key内存超限问题的本质往往不只是“数据量太大”而是“数据量中有一部分永远不会自己走”。比如用户 session 存成 string只写了但是没设过期时间。商品详情缓存set 的时候忘记加EXPIRE。某个统计 key 被不断INCR永不淘汰。这种 key 一旦多了内存就像漏水的水桶即使你设了 maxmemory只要策略是allkeys-lru它会优先淘汰很老但不重要的数据而一堆永不淘汰的“僵尸 key”会持续占用内存。怎么清理这种 key思路很简单找出那些TTL等于 -1没设置过期时间的 key批量筛选出来然后根据业务判断是补一个EXPIRE还是直接删除。# 用 SCAN 遍历 TTL 筛选 redis-cli --scan --pattern * | xargs -I {} sh -c ttl$(redis-cli ttl {}); if [ $ttl -1 ]; then echo {} no expire; fi # 生产环境更推荐写个 Lua 脚本避免大量往返请求但这里有个巨坑如果你用KEYS *去遍历在 key 很多的时候比如百万级会直接导致 Redis 阻塞。我见过同事为了找一个 key跑了一个KEYS user:*结果 Redis 直接卡了 10 秒线上服务超时一大片。所以一定要用SCAN逐批游标遍历每次拿一部分 key 做判断。3.4 应急情况Redis 已经满了数据写不进去怎么办如果你遇到的是线上事故Redis 直接 OOM 拒写了业务已经在报错这时候千万不能慌按这个顺序来第一步降低风险先改成 allkeys-lruCONFIG SET maxmemory-policy allkeys-lru这一步的目的是让 Redis 立刻开始淘汰 key尽快恢复写入能力。注意是“立刻”因为策略的变化是实时生效的Redis 会在下一条命令执行前检查内存并尝试淘汰。第二步定位大 key做精准瘦身redis-cli --bigkeys -i 0.2找到大 key 后可以用UNLINK4.0代替DEL删除它们。UNLINK是异步删除不会阻塞主线程。这个细节很关键一个大 key比如 500MB 的 list用DEL删除Redis 可能卡住一秒以上用UNLINK基本上毫秒级返回后台慢慢清理。第三步看持久化策略别让内存被 AOF 拖死如果你开着appendonly yesAOF 文件会持续增大重写也可能导致内存翻倍因为子进程要 fork 一份内存。这个阶段先确认auto-aof-rewrite-percentage配置如果内存已经很紧张建议临时把auto-aof-rewrite-min-size调大降低重写频率给内存减负。第四步紧急扩容如果是云数据库比如阿里云、腾讯云可以直接改规格但这涉及 IP 变更、连接闪断所以在业务低峰期做。如果是自建加内存不如加节点Redis 集群模式下横向扩容也很自然。4. 常见问题与避坑经验这些坑我全踩过4.1 为什么设置了 maxmemory内存还在涨没有触发淘汰这是最高频的问题也是我最想强调的一点。原因有三个原因一maxmemory 限制的是数据内存不是进程 RSS。刚才说过了复制缓冲区、AOF 重写缓冲区等不在限制范围内。如果你大量执行带大 value 的写操作命令缓冲区堆起来RSS 自然远超 maxmemory。原因二淘汰策略本来就是“内存达到上限之后的下一次写命令才触发”。Redis 不是实时监控内存、一到阈值就立刻清理而是在每次写命令之前检查一下如果超了就按策略淘汰一些 key然后继续执行写命令。所以高峰期写命令非常密集的时候内存会短暂超出 maxmemory超出多少取决于写命令的大小和分布这在 Redis 的设计里是允许的。原因三大 key 无法被淘汰策略“拆分”处理。假设你有一个 1GB 的 string keymaxmemory 是 2GB当前内存是 1.9GB。此时来一个写命令触发淘汰但是所有 key 里只有这个 1GB 的大 key 是 LRU 最久的Redis 会把整个 key 都淘汰掉内存瞬间降下来。但如果这个大 key 是长在某个 list 里的一个元素那没法只剔除这个元素所以大 key 本质上是一种“内存毒瘤”。注意我在实际运维里见过一种情况业务方把图片 base64 编码直接塞进 Redis string一个 key 就几十 MB。这种 key 既不好淘汰也不好迁移最好在业务层就把大对象挪到对象存储Redis 只存“索引字符串”。4.2 Redis 触发淘汰后业务出现大量读 MISS怎么办这是我接手缓存治理项目时最常遇到的现象。一个 Redis 设置allkeys-lru超限后大量缓存 key 被淘汰缓存命中率暴跌请求全部穿透到数据库反而把数据库打挂了。这个问题的根源不是淘汰策略不对而是缓存 key 本身没有设置 TTL。LRU 策略下如果所有 key 都没过期时间Redis 只能依据访问热度去淘汰而访问热度往往是短时波动的。真要防止缓存雪崩应该双管齐下给每个缓存 key 设置合理的 TTL让 Redis 可以稳定地去淘汰“该淘汰的key”。使用volatile-lru让淘汰只集中于有 TTL 的缓存 key不会误杀永久 key。另外要强调一点淘汰不一定是坏事。很多人看到evicted_keys监控指标上涨就紧张其实这是 Redis 在正常工作。真正需要关注的是“淘汰率”是否过高、淘汰之后数据库是否能扛住。我在监控系统里会同时盯三个指标evicted_keys、缓存命中率、数据库 QPS。三者的联动关系才是判断缓存治理是否健康的依据。4.3 分布式锁、队列等业务场景千万不要用 allkeys-lru热搜词里有“redis 分布式锁”我专门提一句。如果你的 Redis 里存有分布式锁、延迟队列、限流计数器这类业务关键数据绝对不能设置allkeys-lru或allkeys-random。为什么因为分布式锁的 key 通常存活时间很短几百毫秒到几十秒而且它在锁竞争期间是“热 key”。但在锁释放之前Redis 内存满了LRU 策略可能把它当成一个“很久没访问”的 key 淘汰掉。一旦锁 key 被淘汰另一线程就能成功获取锁这时候业务上可能发生严重 Bug比如两个线程同时操作同一资源。这类场景的正确配置是maxmemory-policy volatile-lru或者干脆noeviction宁可让获取锁的请求报错也不能让锁 key 被意外淘汰。4.4 用 CONFIG SET 改配置以后重启就失效了很多人改了CONFIG SET maxmemory之后Redis 一旦重启配置又变回默认了。因为CONFIG SET是运行时动态修改默认不会写入配置文件。想让配置持久化有两个办法# 方式一改完 CONFIG SET 后执行以下命令保存到配置文件 127.0.0.1:6379 CONFIG REWRITE # 方式二直接编辑 redis.conf 修改然后重启注意CONFIG REWRITE会把当前配置覆盖写入 redis.conf这个机制本身也可能踩坑。比如你之前用CONFIG SET临时把一个配置改得很小应急用的执行CONFIG REWRITE后就把这个“临时小值”持久化了后续可能一直影响业务。所以CONFIG REWRITE一定要确认当前配置确实是你想要的最终值再执行。4.5 用了 Redis 集群maxmemory 该怎么设置集群模式下每个 Redis 节点是独立的maxmemory是在每个节点上分别设置的。数据按照 CRC16 哈希分布在 16384 个 slot 上每个节点存一部分所以每个节点的内存使用并不均匀。热点 key 多的时候某个节点可能比其他节点先达到 maxmemory。因此集群模式下别把每个节点设成一样的固定值更好的做法是监控每台节点的used_memory和maxmemory的比例动态调整甚至可以考虑给热点节点多加内存因为集群模式下数据不会自动迁移。这个问题在热搜词里也有“k8s redis 集群”容器环境下内存限制就是 Pod 的 limit设置 Redis maxmemory 时一定要留出缓冲别把 maxmemory 设置成正好等于容器内存 limit否则 Redis 本身的内存加上系统开销直接 OOM Kill。5. 一个完整的内存治理方案你可以直接抄作业5.1 从“出现问题才处理”变成“每天自动治理”最高级的治理不是“解决 maxmemory”而是“尽量避免触发 maxmemory”。我给你一套我实践过的组合拳第一层监控告警对三件事做好监控used_memory占maxmemory的比例、evicted_keys增长速率、慢日志中是否出现淘汰相关的执行比如DEL大 key 特别慢。建议在大数据平台比如 Grafana Prometheus配置持久化监控大盘设置比例超过 80% 时告警。第二层定时扫描大 key写一个 cron 脚本每天凌晨低峰期跑一次redis-cli --bigkeys -i 0.2或自研的 SCAN 脚本扫描结果落库。这样你能看到大 key 的历史增长趋势而不是等出问题才知道。第三层自动清理“僵尸 key”对业务 key 统一做规范所有缓存 key 必须带上 TTL。然后定期扫描 TTL-1 的 key筛选出来推给业务负责人确认是补 TTL 还是删。这个流程要产品化不能靠人去查。第四层合理规划淘汰策略结合业务数据特征把 Redis 实例按场景拆分类缓存实例allkeys-lfu 设置合理maxmemory-policy和 TTL。业务数据实例noeviction或volatile-lru只存关键业务数据。5.2 用 Redis 自身命令做一个小型“内存诊断工具”我经常会用以下命令组合对有问题的 Redis 实例做个“体检”# 1. 总体概况 INFO memory # 2. 键空间统计看是否有大量 key 没有 TTL INFO keyspace # 3. 扫描 big keys redis-cli --bigkeys -i 0.2 # 4. 看 CPU 和阻塞情况确认是否因为淘汰导致主线程阻塞 INFO cpu SLOWLOG GET 10看完这些基本就能写出一份内存诊断报告包括内存总量是否充足key 数量是否过多过多则考虑 hash 结构合并是否存在大 key过大则考虑拆分、压缩、或迁移到其它存储是否存在大量无 TTL key需要推动业务整改淘汰策略是否合理结合业务场景调整5.3 缓存穿透、击穿、雪崩与 maxmemory 的联动排查顺带提一下热搜词里的“redis 缓存治理”。当你用allkeys-lru触发大量淘汰后极容易引发缓存雪崩效应。因为淘汰时是批量淘汰而且是按 LRU 顺序淘汰恰好在流量高峰期一大批“冷但即将变热”的 key 被淘汰同一瞬间大量请求穿透到数据库数据库会被击穿。我的建议是淘汰策略上volatile-lru比allkeys-lru更稳至少保证永久 key 不会被淘汰。兜底机制上一定要给缓存 key 设置随机 TTL避免同一批 key 同时过期也避免被 LRU 批量淘汰。业务层上加“空值缓存”结构即使数据库没有数据也在 Redis 写个 key 标记防止恶意请求反复穿透。# 设置随机过期时间防止雪崩 SET product:1001 {} EX 300 random(0, 60)5.4 验证最终结果怎么确认配置生效且稳定所有配置改完后别急着结束花一分钟做验证1. 确认 maxmemory 配置正确127.0.0.1:6379 CONFIG GET maxmemory 1) maxmemory 2) 21474836482. 确认淘汰策略正确127.0.0.1:6379 CONFIG GET maxmemory-policy 1) maxmemory-policy 2) allkeys-lfu3. 用压测工具模拟内存写入放一些固定大小的数据比如每 key 10KB快速写入 20 万个 key观察内存达到 maxmemory 后INFO memory的used_memory是否被控制在目标范围内INFO stats里的evicted_keys是否正常上涨。4. 监控 business QPS 和数据库 QPS确认 Redis 淘汰后数据库 QPS 没有明显飙升业务无异常才算配置真正生效。我个人在实际操作中最大的体会是Redis 出问题从来不是 maxmemory 一个参数的问题而是设计层面的问题。你设了 maxmemory只是给它装了个刹车片真正决定车辆安全性的是你能不能预判路况什么时候加速、什么时候减速。与其每天担心内存超限不如从 key 设计、缓存分层、监控告警上一开始就做对。最后再分享一个小技巧如果你在线环境突发内存满无论如何都不能让业务等最快的操作就是先CONFIG SET maxmemory-policy allkeys-lru让 Redis 活下来然后再坐下来慢慢排查原因。很多人纠结于策略选型在事故现场浪费宝贵的恢复时间这是最不值得的。先保命再优化永远是第一原则。
返回列表