1. 从“加内存”被赶出来说起面试官到底在问什么先还原一下这个让无数后端开发脚趾抠地的场景。面试官问“Redis内存满了怎么办”你心里一乐这题我会资源不够就加资源服务器内存从32G加到64G一了百了。结果面试官脸色一沉让你出去等通知。说实话我第一次听到这个段子的时候也愣了一下因为“加内存”在大多数公司里确实是第一反应。但站在面试官的视角这个问题本质上不是在问你“怎么扩容”而是在考察三件事第一你知不知道Redis的内存并不是无限膨胀的它有自己的上限第二内存满了之后Redis会有什么表现是拒绝写入还是腾挪空间这里面的机制你是否清楚第三你有没有系统性地排查过内存消耗的真实来源而不是一拍脑袋就砸钱上硬件。“加内存”这三个字之所以让人反感是因为它绕开了所有技术细节属于典型的“用钱解决问题”而面试官想听的是“用脑子解决问题”。而且这里还有一个隐性的业务场景考量。在真实的线上环境里Redis通常是几十上百个业务共用的或者是一个大集群中的某个分片。你贸然加内存成本只是一方面更麻烦的是流量倾斜、数据分布、持久化策略等一堆连锁问题都会跟着变。所以面试官真正想听到的回答应该是一套完整的“先诊断、再治理、最后才扩容”的思路而不是一句轻飘飘的加内存。换句话说这个问题从你嘴里说出来的第一句话就应该体现出你知道Redis内存问题的复杂性。我自己在带团队的时候也经常拿这个问题考组里的新人。答案倒不是唯一的但那些能说出“先看info memory确认内存分配情况再用redis-cli --bigkeys扫大key然后分析淘汰策略配置”的人和只会说“加内存”的人对系统的理解深度确实不在一个量级上。2. 内存满之前的体检先把“内存去哪了”这件事搞清楚很多人把“内存满”理解成一个突然到来的灾难其实不是。Redis的内存占用是一个渐进的过程从内存水位升高到真正写入失败中间有很长一段窗口期可以做诊断。问题是你得先知道去哪里看、看什么。2.1 第一站永远是info memory不管哪个版本的Redisinfo memory都是最直接的入口。里面有几个关键指标我建议每个做Redis运维的同学都刻在脑子里。used_memory是Redis分配器实际分配出去的内存总量包含数据本身、过期字典、客户端缓冲区、复制积压缓冲区等等。used_memory_human是它的可读版本一眼就能看出占用规模。used_memory_rss是操作系统视角下Redis进程占用的物理内存这个值通常比used_memory大因为包含了内存碎片。mem_fragmentation_ratio就是用二者相除得到的碎片率。我一般在看内存问题的时候第一个动作就是盯mem_fragmentation_ratio这个比值。如果它在1到1.5之间说明内存分配是健康的如果超过1.5说明碎片化已经比较严重了如果小于1那就意味着Redis的部分内存被换到了Swap上这种情况反而更危险说明物理内存真的不够用了连碎片都塞不下。还有一种情况很容易被忽略used_memory和used_memory_rss都正常但系统内存还是被打满这时候就要去查used_memory_peak看看历史峰值是不是远高于当前值。这能帮你判断是不是曾经有过一波流量高峰把内存冲上去之后又没有释放干净。Redis并不会把已经申请到的内存轻易还给操作系统这跟JVM的堆内存其实是一个道理。2.2 从大key、过期key到客户端缓冲区逐个排查查完整体水位就要开始定位内存的具体去向。我最常用的命令是redis-cli --bigkeys它会遍历整个实例按类型统计出最大的那些key。这不是一个精确的统计但作为排查起点已经够用了。这个命令有几个输出维度最大的string、最大的list、最大的hash、最大的set、最大的zset每个类型都会给出具体的key名和大小。扫完大key之后还要关注另一个非常隐蔽的内存黑洞——过期的key。Redis的过期删除是惰性删除加定期删除的组合惰性删除就是访问的时候发现过期了才删定期删除是后台每秒抽几次检查删除。这意味着大量设置了TTL的key在同一时间过期时删除是有延迟的可能几分钟内内存都不会明显下降。如果业务里习惯性地给key设置24小时过期然后每天凌晨定时批量写入内存曲线就会呈现锯齿状峰值时刻系统压力非常大。客户端缓冲区也是很多新手忽略的点。Redis对每个客户端连接都维护了输入缓冲区和输出缓冲区输出缓冲区尤其危险因为如果客户端消费速度跟不上而Redis还在不断往缓冲区里写数据输出缓冲区就会无限膨胀。我记得Redis 6.0之后对client-output-buffer-limit做了不少优化但默认值对某些大key批量读取的场景依然不够。排查的时候可以用client list看每个连接的omem字段这个字段就是输出缓冲区的当前占用。我把这几个诊断项的排查逻辑整理成了一张表方便大家对照自查诊断项查看命令异常信号通常原因整体水位info memoryused_memory接近maxmemory数据量增长或缓存未过期内存碎片info memorymem_fragmentation_ratio 1.5频繁更新、删除、大key释放大keyredis-cli --bigkeys单个key超过预期列表积压、未分页查询结果集过期key堆积info stats中expired_keys增量内存下降滞后集中过期、删除策略触达不够客户端缓冲区client listomem异常大消费速度跟不上生产速度峰值内存info memoryused_memory_peak远超used_memory高峰期大流量写入后未释放2.3 用memory doctor做一次自动化体检除了手动一条条敲命令Redis还内置了一个自动诊断工具redis-cli --memory doctor或者MEMORY DOCTOR。它会自动检查内存碎片率、峰值内存、客户端缓冲区等指标然后给出诊断报告。虽然它的判断比较保守有时候会给一些无关痛痒的建议但胜在快适合作为第一轮的快速筛查工具。我自己在线上排查的时候通常的做法是先跑MEMORY DOCTOR和redis-cli --bigkeys拿到初步结论然后针对异常项再用MEMORY USAGE去精确测量某个key到底占多少字节。MEMORY USAGE后面可以跟SAMPLES参数对大集合类型可以指定采样数量避免全量计算造成阻塞。这一步很关键因为--bigkeys给出的只是“大”的初判具体大到什么程度、释放它之后能省多少内存还是得靠MEMORY USAGE来精确量化。3. 内存淘汰不是玄学Redis的淘汰策略和触发逻辑诊断做完之后如果你发现内存确实是在合理范围内不够用了——也就是数据都是有用的、无法再压缩——那就要聊聊淘汰策略了。这是面试官最想听到的第二个层次也是整个问题里最核心的机制知识点。3.1 maxmemory限额不设等于裸奔很多中小团队部署Redis的时候压根不设置maxmemory觉得机器内存多大就能用多大这是极其危险的。因为Redis在64位系统上默认的maxmemory是0也就是不限制内存使用。这意味着如果没有设置限额Redis会一直吃内存直到操作系统OOM Killer把它杀掉。而且由于Redis是单线程模型内存分配和释放都阻塞在主线程里一旦操作系统开始频繁Swap换页Redis的响应时间会呈指数级恶化表现为连接超时、读写卡顿但看起来又不像宕机。正确的做法是在配置文件里显式设置maxmemory比如maxmemory 4gb。设置之后当Redis的内存使用量达到这个值写入命令就会开始触发淘汰逻辑而不是直接报错。这个限额应该怎么定我一般建议设置为机器物理内存的50%到60%之间同时要预留出持久化fork子进程的内存开销尤其是启用了AOF重写或者RDB快照的实例fork瞬间会有一份copy-on-write的内存复制开销上限设得太满很容易在持久化时触发OOM。3.2 八种淘汰策略的适用范围Redis的淘汰策略分布在两个维度的交叉点上一个是作用范围是只淘汰设置了过期时间的key还是所有key都能淘汰另一个是淘汰算法是LRU还是LFU还是直接随机。noeviction是默认策略内存满了之后写入直接报错不影响读。这也是很多线上事故的导火索——业务方一直以为Redis写不进去是因为网络或者代码问题结果一查提示OOM command not allowed when used memory maxmemory。这个报错信息我见过太多次了。allkeys-lru和volatile-lru是好兄弟前者对所有key做LRU淘汰后者只对设置了过期时间的key做LRU淘汰。这里有个很重要的点Redis的LRU其实是近似的不是严格的。它不会维护一个全局的访问时间链表而是给每个key记录一个24位的LRU时钟然后随机采样几个key淘汰其中最近最少使用的那个。这个采样数量可以通过maxmemory-samples配置默认是5调大到10会提高淘汰的准确性但代价是稍微多一些CPU消耗。allkeys-lfu和volatile-lfu则基于LFU算法它不只是看“多久没访问”而是统计访问频率适合那种某些key会被高频访问、但访问模式并不符合纯LRU假设的业务。LFU的核心是维护一个Morris计数器随着时间衰减访问频次8位的计数器从0增长到255。它的优点是对低频但长期被访问的key更友好不会因为一段时间没访问就被误杀。allkeys-random和volatile-random就是随机淘汰适合那些对缓存命中率完全不敏感、只求不报错的场景。volatile-ttl则是根据剩余TTL来淘汰哪个快过期淘汰哪个但这个策略在实际使用中很少见因为它需要维护额外的TTL信息效果也不如LRU直观。我把这些策略的适用场景归纳一下策略淘汰范围淘汰算法建议使用场景noeviction无不淘汰数据不能丢的队列/锁场景volatile-lru有过期时间LRU近似缓存部分持久化key混合allkeys-lru全部keyLRU近似纯缓存场景allkeys-lfu全部keyLFU访问频率差异大的缓存volatile-lfu有过期时间LFU热点key访问极不均匀allkeys-random全部key随机命中率不敏感volatile-random有过期时间随机淘汰任意过期key都行volatile-ttl有过期时间剩余TTL最小不推荐效果一般3.3 淘汰策略选错会出什么人命关天的事聊到淘汰策略必须提醒一个业务上的大坑。如果你把Redis当成数据库来用存了订单数据、用户余额、分布式锁等关键数据然后配了一个allkeys-lru的淘汰策略那么当内存满的那一刻最先被淘汰的可能是你的锁key或者余额数据后续业务逻辑就会连环出错。我在实际工作中见过一个支付回调服务把回调状态直接放Redis设置了allkeys-lru结果高峰期内存满了淘汰了一部分回调状态key回调方以为Redis丢了数据重推下游幂等逻辑还过期了结果出现重复入账的风险。排查了一整天才定位到是淘汰策略导致的。所以设置淘汰策略之前先回答一个问题这个Redis实例里的数据丢了会怎么样如果会出事就必须保证这类key不被淘汰要么单独部署一个实例要么用noeviction策略配合报警要么在业务层面给关键key设置独立的过期时间。纯缓存场景才适合allkeys-lru这个边界一定要搞清楚。4. 真正省内存的实操从数据模型到配置项的优化清单排查和淘汰策略都属于“兜底”真正让Redis内存长时间保持在健康水位靠的还是日常的优化习惯。这一节我分享一下我在生产环境里沉淀下来的、实实在在能压内存的做法很多都是在踩坑之后才总结出来的。4.1 大key治理尤其是Hash和List大key是Redis内存的第一杀手。一个包含几百万字段的Hash或者一个长度几百万的List不仅占内存还会在扩容、缩容时引发内存抖动。我印象很深的一个案例是某个排行榜服务用ZSet存了一年的排行榜明细一个key下面几百万个member单个key的内存超过200MB。每次这把key被访问Redis的响应时间都比正常key慢好几个数量级因为ZSet的底层skip list查找是O(logN)的数据量大了之后单次访问的CPU开销非常可观。治理大key的思路通常是拆分。Hash可以用HSCAN分页遍历后按照业务维度拆成多个小key比如按照日期、按照用户ID范围、按照业务模块拆分List则要想清楚它到底是用来做队列还是做缓存如果是队列应该考虑用Stream或者专业的MQ来替代Set和ZSet的拆分思路类似本质上都是“分桶”把一个巨大的集合拆成很多个小集合。还有一种情况是存储了过大的value比如直接把一个几MB的JSON字符串塞进Redis。这种问题的解法是先在应用层做数据裁剪只存需要的字段不要偷懒把整个对象序列化进去。如果真的需要存大对象可以考虑压缩比如用Snappy压缩后再写入但要注意CPU消耗和压缩收益之间的平衡。4.2 用对数据类型和编码轻松省掉一半内存Redis每个数据类型底层都有多种编码方式这是很多人容易忽略的内存优化点。比如Hash在字段数量少且值较小的时候用的是ziplist编码省内存神器超过阈值之后切到hashtable编码内存开销会显著上升。List、ZSet也都有类似的紧凑编码。你可以用OBJECT ENCODING命令看一个key当前用的编码。比如OBJECT ENCODING myhash返回ziplist还是hashtable就知道有没有触发编码转换。在Redis 3.2版本中List的底层结构换成了quicklist把ziplist和linkedlist的优点做了结合每个节点是一个ziplist像一个链表串起很多个小段既能节省内存又保留了双向链表的高效插入删除特性。这里有个小技巧如果你的业务中Hash字段数量比较多建议主动控制每个Hash的字段数。一般来说让ziplist编码的阈值控制在合理范围内比如128个字段内且每个字段值小于64字节内存效率是最高的。如果超过阈值切换到hashtable之后单个字段的平均内存开销会从几十字节飙升到上百字节这种翻倍是肉眼可见的。4.3 过期时间不是摆设批量过期才会出事我见过大量生产事故是因为缓存“永远不过期”引起的。很多业务同学图省事写入Redis的时候不设TTL结果数据越积越多内存稳步上涨直到某一天触发运维报警。这种问题其实在代码评审阶段就应该被拦下来但如果历史代码已经写进去了处理起来也不难可以用SCAN配合EXPIRE在低峰期批量设置过期时间。批量设置过期时间的时候要千万注意不要用KEYS *遍历会阻塞Redis主线程。一定要用SCAN命令做增量迭代每次拿一部分key然后处理循环往复直到遍历完成。SCAN的cursor参数是游标式遍历比如第一次返回的游标是0说明扫描完了否则把返回值当作下一次SCAN的参数传进去。再加上COUNT参数控制每次扫描的key数量比如100这样对实例的影响就很有限了。同时也要注意集中过期的问题。如果大量key设置的是同一个过期时间点比如统一在凌晨3点过期那么3点到3点5分这个时间窗口内Redis的CPU会被过期删除任务打满表现为间歇性卡顿。解决方案是把过期时间做一个随机偏移比如基准过期时间是24小时实际设置的时候加上0到3600秒的随机值让过期时间均匀分布在一天之内。这个思路跟批量更新缓存时加随机延迟防止缓存雪崩是一个道理。4.4 内存碎片整理与maxmemory-policy的联动碎片问题是Redis内存优化的一个老大难。频繁更新、删除大key、值的大小差异较大都会让内存分配器产生碎片。碎片率mem_fragmentation_ratio超过1.5的时候就该处理了。Redis 4.0之后引入了主动碎片整理机制配置项是activedefrag yes它会自动在后台移动内存页来减少碎片。但这个功能有个副作用它会增加CPU消耗需要配合active-defrag-ignore-bytes和active-defrag-threshold-lower来控制触发的阈值。我自己一般设置碎片率超过1.5且碎片字节数超过100MB才开始整理整理过程中如果CPU占用过高还会自动退让。不过更经济和稳妥的方式还是从源头控制碎片把Redis实例的数据分布做得更均衡避免某个分片频繁更新而其他分片只读大key尽量不要频繁修改如果碎片问题反复出现且无法通过配置解决就要考虑运维层面的方案比如主从切换后重启实例重启之后内存会重新分配碎片率就会归零。这也是为什么有些团队会定期做实例重启来“降碎片”。5. 什么情况下才轮到“加内存”出场前面讲了这么多排查、优化、淘汰的手段但你千万别把“加内存”一棒子打死。加内存本身不是错错的是把加内存当成第一选择而不是最后选择。那么什么情况下加内存才是合理的解法这一节说说边界。如果你的Redis已经做了以下优化依然扛不住内存压力那么加内存就是正确的方案第一数据模型已经优化过大key已经拆分过期时间已经设置该压缩的也压缩了但业务增速实在太快比如日活用户翻倍、新增的榜单和推荐位数据量远超预期这种属于业务正常增长带来的需求加内存是顺应业务发展的合理投入。第二淘汰策略已经明确定义但你不想淘汰任何数据因为每一个key背后都是业务必须的。这种场景下加内存其实是在规避淘汰风险。比如你在Redis里存了会话和分布式锁如果内存不足导致锁key被淘汰分布式锁这种跨节点的互斥机制会迅速失效引来的麻烦远远超过内存扩容的成本。第三峰值内存远超均值而且峰值是不可削平的。有一些业务天然存在“流量尖峰”比如商品秒杀、大型促销0点那一瞬间的写入量是平时的十倍二十倍。这种场景加内存的意义在于让Redis能扛住峰值流量而不触发淘汰毕竟你不能要求业务在高峰期少写点数据。但加内存也不是说加就加有几个运维层面的问题必须先想清楚。单机加内存意味着单点风险加大机器挂了更多数据在内存里没持久化恢复时间会更长Redis的fork持久化子进程在内存越大的机器上copy-on-write的开销也越大单实例内存太大后续做分片迁移、数据恢复、全量同步都会更慢。所以正确做法是如果单实例内存超过16G甚至32G就应该考虑走集群方案用多个实例分摊内存压力而不是无限堆单机内存。我遇到过的最合理的加内场景是一个广告投放系统每天的曝光数据和点击数据以亿为单位累积模型已经优化过三轮TTL也设置了但数据保留周期是硬性的业务要求不能缩短。这种情况下再怎么做瘦身都是徒劳必须扩容。当时我们从32G加到64G内存问题直接解除后续又把热点数据挪到了Redis Cluster的多分片上算是彻底根治。所以“加内存”和“加内存”是不一样的不经过任何诊断甩出“加内存”三个字这叫拍脑袋基于完整的监控数据、优化记录和业务评估得出“需要扩容”的结论这叫工程决策。面试官要的从来不是那个结论而是你做结论的过程。6. 一套能过关的面试回答拆解最后聊聊面试场景。如果现在又被问到同样的问题“Redis内存满了怎么办”我会建议你按照“诊断——治理——扩容”三层来答每一层都带上具体的命令和指标让面试官顺着你的思路听到最后。第一层先说诊断习惯我会先跑info memory看used_memory、used_memory_rss、mem_fragmentation_ratio目的是确认现在的内存是数据真的多还是碎片化严重。然后会redis-cli --bigkeys扫一遍把大key找出来看一下是不是某个业务方写入了一个超级大的集合或者超大的value。这一步可以带上你实际遇到过的案例比如某个排行榜ZSet有上百万个member单个key超过100MB这类细节一说出来面试官基本就能确认你有实战经验。第二层说治理手段如果诊断出来是数据模型问题就讲怎么拆分大key、怎么优化数据结构、怎么设置合理的TTL并做随机偏移、怎么用MEMORY USAGE精确测量。如果是碎片问题就讲activedefrag和实例重启降碎片的方案。同时还要提一下淘汰策略的选择说明你是根据业务容忍丢失的程度来确定用allkeys-lru还是volatile-lru还是noeviction。第三层才是扩容但要说得很谨慎如果以上手段都用完了数据是硬需求业务增长确实需要更多内存才会考虑扩容。单机内存有限制优先通过Redis Cluster做水平分片而不是一味加单机内存。加内存的过程中要考虑持久化fork开销、数据恢复时长、迁移成本这些隐性代价。这个回答结构的好处是它是一个完整的思考链路从问题发现到问题定位从临时方案到根本治理再到最后的手段层层递进。面试官听到的不是一个“填空题答案”而是一个真实的项目经理在复盘整个处理过程。另外补一句面试的时候别慌张问题本身并不是真要你立刻解决一个线上故障而是看你的知识体系是否完整、经验是否成体系。哪怕你最终只记住了info memory和--bigkeys这两个命令并能把它们用到自己的回答里这道题就已经回答得比网上大多数人好了。