Redis缓存设计:用20%热点数据扛住80%请求

📅 发布时间:2026/9/19 14:27:42
Redis缓存设计:用20%热点数据扛住80%请求
做后端这些年Redis缓存应该是绕不开的一环。面试会被问线上调优会碰到真出问题的时候第一个被拎出来审查的也常常是它。很多人觉得缓存不就是把热点数据往内存里一放嘛但真正想把Redis用得有价值核心其实就一句话让20%的热点数据去接住80%的请求。这句话不是玄学背后是数据访问的幂律分布是容量规划、过期策略、淘汰策略、一致性方案层层叠加的结果。这篇就围绕这个主题把Redis缓存的完整链路拆开讲清楚适合正在做缓存优化、准备高并发面试或者想搞明白缓存设计到底该怎么落地的后端同学。1. 先聊聊为什么“20%数据应对80%请求”能成立1.1 访问分布不平均才是常态一个商城你可能觉得所有商品都有上架价值但实际上顾客进门后直奔的就是那几款爆品。线上系统也一样绝大多数业务的访问量都不是均匀铺开的而是集中在少数几个关键对象上电商网站的爆款商品、内容平台的热门文章、社交App里的头部大V这些对象贡献了绝大部分流量。我在实际项目里统计过一组数日均几十万PV的电商站商品SKU过百万但每天真正有用户访问的SKU可能就几万个再看实时流量头部十几个商品在活动期间能占到全站请求的30%以上。用算法的话说这符合幂律分布就是常说的长尾效应。二八定律在这里的描述是80%的请求集中在20%的数据上。注意这不是精确的物理定律而是一种经验规律具体比例因业务而异有的系统是10%数据扛了90%请求有的则是30%扛了70%但大方向是一致的。理解这一点对做缓存设计很关键。缓存之所以能大幅提升系统性能不是因为内存查询比数据库快了多少而是因为缓存把最稀缺的计算资源——数据库连接、磁盘IO、查询时间——只花在真正需要它的那部分请求上。1.2 从成本角度量化“为什么要用Redis缓存”数据库查询和Redis查询的代价差距可以用一组直观数字说明。一个典型的MySQL单行主键查询在索引命中的情况下局域网内平均耗时约2至10毫秒如果是复杂条件查询或者数据量大几十毫秒上百毫秒都很正常。而Redis基于内存操作单次读写通常在0.1到0.5毫秒的量级差距是几十倍。再看并发能力。一个4核8G的MySQL实例合理配置下能稳定支撑的读QPS大概在几千到一两万而同样规格的机器上部署Redis单实例QPS能到十万以上瓶颈往往在网卡和客户端不在Redis本身。所以当系统流量上来数据库连接池先被占满这时候最直接的解法不是无限扩容数据库而是把热数据往前挪挪到Redis这个更快的存储层。这就引出一个设计思路缓存的价值不在于“把所有数据存一份”而在于“用有限的内存覆盖绝大部分请求”。内存是贵的Redis数据全放内存更是如此所以“覆盖哪些数据”就成了缓存设计的第一性问题。答案就是那20%。1.3 20%这个数字应该怎么解读很多面试官爱问Redis缓存设计其实他们想听的就是你怎么理解这个二八分布。注意20%不是让你真的去数20%而是强调一种决策导向缓存绝不应该是全量数据的镜像而应该是热点数据的快照。我在设计缓存容量时有个习惯先回答三个问题当前业务里每天被访问的key数量级是多少这些key里头部高访问频率的key大概占多大比例单条缓存数据的平均大小是多少把这三个问题的答案乘一乘就能得出一个比较靠谱的缓存容量预算。如果业务每天被访问的活跃key是50万平均每条value是2KB那么头部20%的10万条数据大概占用200MB内存这在一个4G内存的实例上毫无压力。可如果不管三七二十一把所有数据都塞进去内存再怎么加也不够用。这部分就是整个缓存设计的地基。地基打好了后面聊热点识别、淘汰策略、一致性方案才有讨论的坐标系。2. 找出那20%的热点数据别拍脑袋2.1 从访问日志和监控指标里筛热点设计缓存的第一步不是写代码而是搞清楚你的热点在哪里。最直接的办法是分析访问日志。Nginx的access log通常记录了完整的请求路径按URL聚合统计一下访问次数就能大致知道流量集中在哪些资源上。以一条商品详情页的访问路径为例/product/10086统计访问频次可以用awk一行搞定awk {print $7} access.log | sort | uniq -c | sort -rn | head -20这条命令的作用是把日志里第7个字段通常是请求URL取出来去重统计次数按次数倒序排出前20个。每次做数据迁移或大促前我都会跑一遍这个命令看最近一周的流量集中度。如果头部20个URL占了总请求量的50%以上那说明二八分布确实存在缓存设计有明确的目标。除了Nginx日志Redis自身也能提供一些统计信号。INFO stats这个命令会返回两个关键指标keyspace_hits缓存命中次数和keyspace_misses缓存未命中次数。用这两个数值可以算出缓存命中率命中率 keyspace_hits / (keyspace_hits keyspace_misses)这个指标是所有缓存优化的核心KPI。命中率上不去加再多内存都是浪费。一般来说读多写少的业务场景缓存命中率应该稳定在90%以上如果低于这个值就要回头审视热点识别是否准确、TTL是否设置得太短、淘汰策略是否误杀了热点数据。2.2 Redis自身提供的热点发现手段业务日志分析只能看到URL维度的热度要深入到Redis的key维度就得用Redis提供的工具。第一个是redis-cli --hotkeys。这个命令需要先开启LFU淘汰策略也就是在redis.conf里配置maxmemory-policy allkeys-lfu或volatile-lfu然后执行redis-cli --hotkeys它会扫描整个key空间按访问频次排序把热key列出来。不过要注意这个命令的原理是遍历所有key在key数量很大的实例上执行时会有一段时间的阻塞风险建议在业务低峰期跑或者用scan配合OBJECT FREQ命令自己写一个低峰期扫描脚本。第二个是redis-cli --bigkeys虽然这是查大key的工具但大key和高频key常常是重合的。大key意味着单条数据访问成本高、网络传输慢如果它恰好是热点对整个集群的影响会被放大。--bigkeys同样建议在低峰期执行它会按key类型分别列出最大的几个key。还有个偏门但很有用的方法用MONITOR命令抓取一小段时间窗口的实时命令流把窗口内的GET命令按key聚合。注意MONITOR在生产环境上不能长时间开会显著降低Redis吞吐但短时间抓取几秒钟、几百个请求用来辅助判断热点key是完全够用的。我在排查热点key引起的CPU飙升时用这种方式最快定位到具体是哪个key出了问题。2.3 给热点数据建立“画像”找到热点数据之后不要急着写缓存先给它们建一张画像表。我习惯从以下几个维度记录key的名称和所属业务模块value的类型和大小当前TTL剩余时间预估QPS数据变更频率必要的时候问一下上游这个数据多久更新一次关联的接口和调用链这张画像表的意义在于缓存策略的所有参数都要根据画像来定变更频繁的数据TTL要短否则缓存与数据库一致性问题会变严重大小以KB为单位的大value要考虑网络序列化开销QPS极高的key要考虑是否需要在本地缓存再挡一层。没有画像所有配置都是盲调。3. 缓存参数设计容量、过期与淘汰3.1 容量规划到底该给Redis配多少内存容量规划是缓存设计里最容易被忽视的一环。很多团队上来就配个默认的512MB或1GB等线上出现内存淘汰时才发现热点数据被清掉了又拍脑袋往上加。正确的做法应该和目标命中率挂钩。我常用的方法是从“热点集合大小”反推内存。假设你已经识别出头部20%的活跃key有10万个平均value大小是2KB那么这部分数据占用的内存大约是200MB。为了给数据波动留余量我会再乘以1.5到2倍的系数也就是预留400MB左右。如果实例总内存是4GB给Redis分配512MB或1GB都够用剩下的留给操作系统页缓存和扩展空间。这里有个值得强调的点maxmemory的配置直接影响淘汰策略的行为。当Redis内存达到maxmemory上限后写命令会触发淘汰策略如果配的是noeviction写操作直接返回错误。我在很多生产环境看到过类似故障缓存写入突然报OOM一查发现maxmemory设置得远小于实际数据占用而策略又是默认的noeviction。所以容量规划不是调一个数字那么简单要从数据量级和服务等级目标反推。3.2 过期策略固定TTL是个隐形炸弹缓存过期时间设计得好不好直接决定了系统在大流量下会不会出事故。最常见的问题是所有key都用同一个固定TTL比如统一设置了30分钟。表面上看很合理但仔细想想如果一批key在同一个时间段写入那它们也会在同一个时间段过期Redis里就会周期性出现“缓存短暂真空期”大量请求在同一瞬间穿透到数据库。这就是缓存雪崩的一种典型场景。解决思路很简单给TTL加随机偏移。比如基础过期时间设为30分钟实际设置时在此基础上加一个0到300秒的随机数TTL 1800 random(0, 300) 秒这样同一批写入的key过期时间被自然打散数据库就不会在同一瞬间收到成片的回源请求。这个技巧成本极低效果立竿见影我在项目里从第一次缓存设计就强制执行这一条。还有一类特殊的key不适合用物理过期时间典型的就是“热度极高、更新频率低”的数据。比如某个爆款商品的详情它可能持续热一周但过期时间设太长又担心数据变更后缓存不更新。这种情况可以用“逻辑过期”方案不设置TTL把过期时间作为业务字段放进value里比如{expire_at: 1699999999, data: {...}}。读取时检查expire_at发现过期就异步重建缓存同时先返回旧值。这样做的好处是永远不会出现缓存key被Redis物理删除导致的击穿坏处是代码逻辑会复杂一些需要考虑清楚重建时机和并发控制。3.3 淘汰策略选型让Redis自己留下热点当内存达到maxmemory上限时Redis需要决定先赶走哪些key。这几种策略各有适用场景我用一张表梳理清楚策略含义适用场景noeviction不淘汰写命令直接报错把Redis当数据库存关键数据不允许丢keyallkeys-lru从所有key中淘汰最近最少使用的纯缓存场景所有key都允许淘汰allkeys-lfu从所有key中淘汰访问频率最低的纯缓存场景访问频率分布极不均匀volatile-lru从设置了TTL的key中淘汰最近最少使用的部分key不能丢比如分布式锁部分可淘汰volatile-lfu从设置了TTL的key中淘汰访问频率最低的同上且热点集中明显volatile-ttl从设置了TTL的key中淘汰剩余时间最短的很少用TTL快到的数据大概率也快失效我的选择建议分两种情况。如果Redis实例只用来做缓存所有key都是可重建的我推荐allkeys-lfu。因为LFU比LRU更贴近二八分布的淘汰逻辑它能精准识别出“一直被访问”的数据而不是“最近刚被访问过”的数据。一个典型的反例是某数据只在每天凌晨被定时任务批量访问一次白天几乎没有流量LRU会误以为它是热点给它留在内存里LFU则能更好地区分这种脉冲式访问和真正的持续热度。Redis 4.0之后才支持LFU如果版本太老退而求其次用LRU。如果Redis里混合存了缓存和不可丢失的数据比如分布式锁的key那就必须用volatile-*系列策略。这样Redis只会淘汰设置了TTL的key没设TTL的key不受影响锁的key没有TTL就不会被清掉。注意使用volatile-*策略的前提是所有可淘汰的缓存key都必须设置TTL否则这些key永远不会被淘汰内存很快就爆了。4. 高并发下的缓存击穿、穿透与雪崩4.1 三类经典故障的辨析与应对缓存层引入后系统多了一个数据源也多了三类典型的分布式系统故障缓存穿透、缓存击穿、缓存雪崩。这三个概念经常被搅在一起其实本质完全不同。缓存穿透指的是查询一个必然不存在的数据。比如攻击者抓包后把商品ID改成-1数据库里根本没有这个商品Redis里也没有每次请求都穿透到数据库。如果攻击者用大量不存在的ID并发请求数据库只能被动扛下全部流量。解决穿透的办法有两个一是布隆过滤器在缓存之前加一层判断如果ID不可能存在就直接返回二是缓存空对象把查询结果为空的key也写进缓存设置一个较短的TTL比如60秒这样同样的无效请求在短时间内不会再打到数据库。缓存击穿指的是一个热点key刚好过期而就在这一瞬间有成百上千的请求同时来查这个key。由于缓存里没有数据所有请求一起涌向数据库。这个问题的核心不是数据不存在而是“热点key的重建过程没有做并发控制”。解法是互斥锁在重建缓存时只允许一个线程去数据库查询其他线程等待重试或者直接返回降级数据。互斥锁的实现要用分布式锁不能只用本地锁因为服务通常是多节点部署的。这里给出一个Java侧的互斥重建模板我在多个项目里用过逻辑很直观String value redis.get(key); if (value null) { String lockKey lock: key; boolean locked redis.setnx(lockKey, 1, 3, TimeUnit.SECONDS); if (locked) { try { value db.query(key); if (value ! null) { redis.set(key, value, ttl randomOffset); } else { redis.set(key, NULL, 60); // 缓存空值防穿透 } return value; } finally { redis.del(lockKey); } } else { // 没抢到锁短暂sleep后重试或者直接走降级 Thread.sleep(50); return getFromCacheWithLock(key); } }缓存雪崩则和击穿不一样它是指大量key在同一时间段集体过期或者Redis实例本身不可用导致所有请求都砸向数据库。应对雪崩的手段是多层次的TTL加随机偏移是最基本的一招更保险的做法是搭建Redis高可用架构主从复制加哨兵或者直接用Cluster模式让单个Redis节点的故障不至于整体不可用再进一步可以在Redis之上再加一层本地缓存比如Caffeine即使Redis整体挂了本地缓存还能消化掉一部分请求给数据库争取喘息时间。4.2 缓存与数据库的一致性方案选型缓存和数据库之间的一致性是缓存设计里讨论最多的问题。为什么会有不一致因为数据有两个存储位置更新动作不可能原子完成总要有个先后顺序。主流做法是Cache Aside模式也就是读缓存、未命中读数据库、写缓存更新数据库、删除缓存。这里有个关键更新数据库时不要直接更新缓存而是把缓存删掉等下次读取时再重建。原因在于直接更新缓存容易产生并发写穿插导致缓存里留下旧数据删除则相对安全哪怕删除失败导致缓存命中旧数据也最多是短暂不一致。删除缓存后还会遇到一个新的时序问题某个线程更新完数据库删除了缓存但另一个线程在删除之前读到了旧值正在把旧值写回缓存。这个旧值写回后缓存里就长期留着过期的数据。解决这个问题业界有一套“延迟双删”的经验做法更新数据库后删除缓存等待几百毫秒再次删除缓存。这个等待时间一般是让可能发生的“旧值写回”操作完成第二次删除把它清掉。延迟双删的伪代码时序是这样的服务A查询缓存未命中从数据库读到旧值v1。服务B更新数据库把数据改为v2。服务B删除缓存。服务A把读到的v1写回缓存。在没有延迟双删时第4步会让缓存里长期是v1数据库里是v2。加了延迟双删服务B在第3步之后等待n毫秒再删除一次缓存将步骤4写入的v1清掉。不过延迟双删也有局限性它依赖一个经验性的等待时间不同环境里“旧值写回”的时间并不固定。更彻底的方案是订阅数据库的binlog变更比如用Canal监听MySQL的binlog把数据变更事件推到消息队列消费者异步删除或更新缓存。这样做的优势是更新缓存和删除缓存的动作不再依赖业务代码在事务里手动调用数据变更本身就会触发缓存刷新一致性窗口可以做得更小。在方案选型上我的经验是大多数业务场景都接受最终一致性没必要追求强一致。强一致方案要么引入分布式事务复杂度直线上升代价太大。对普通读多写少的业务Cache Aside加延迟双删已经够用如果对一致性要求比较高再加binlog订阅兜底。过度设计的成本往往比数据短暂不一致带来的风险更高。4.3 高并发兜底设计本地缓存、限流与降级Redis再快也不是万能钥匙。极端情况下Redis的QPS本身也会被打满或者网络延迟成为瓶颈。所以设计方案时一定要考虑兜底不能把全部鸡蛋放在一个篮子里。我常用的分层思路是Caffeine本地缓存兜底第一层Redis兜底第二层数据库兜底第三层。Caffeine是Java生态里性能很出色的本地缓存库读写都在进程内没有任何网络开销。对于QPS极高的热点key本地缓存能有效拦截大量请求让Redis只承接那些本地缓存未命中的请求。不过这也要控制好容量和一致性本地缓存的TTL通常设得很短比如30到60秒避免不同节点数据差异过大。限流和降级也是必须做的。在网关层对缓存回源的请求做限流比如每秒只允许一定数量的请求落到数据库多余的请求要么排队等待要么直接返回用户兜底数据比如默认配置、上次的缓存快照等。这个兜底数据可以在服务启动时加载到内存保证即使下游全部不可用系统也不至于一个请求都响应不了。5. 实操案例商品详情页缓存从零到一5.1 场景假设与设计目标把以上理论落到一个具体的场景里。假设电商平台的商品详情页接口读写比例大约100:1峰值QPS是2万数据库单实例最大支撑3000 QPS一旦超限就会拖垮其他业务。我们的目标很清晰通过缓存设计把数据库的查询压力控制在1000 QPS以内让系统在高峰期游刃有余。第一步先做热点分析。统计最近一周的Nginx访问日志发现头部20%的商品ID覆盖了全站大约75%的详情页请求。这个比例非常典型说明二八分布在这里成立。第二步统计这些热点商品的数据大小平均详情JSON序列化后约2KB。第三步确定容量规划假设热点集合约10万条加上冷备数据共30万条每条平均2KB总内存占用量约600MB分配1GB给Redis实例绰绰有余。5.2 核心链路代码实现商品详情页的读取链路按三级设计先读Caffeine本地缓存未命中再读Redis仍未命中才回源数据库同时用分布式锁防止并发回源。这里给出简化版的实现public Product getProduct(Long id) { // 第一级本地缓存 Product product localCache.getIfPresent(id); if (product ! null) { return product; } // 第二级Redis缓存 String key product: id; String json redis.get(key); if (json ! null) { if (NULL.equals(json)) { return null; } return JSON.parseObject(json, Product.class); } // 第三级数据库兜底 互斥重建 String lockKey lock:product: id; boolean locked redis.setnx(lockKey, 1, 3, TimeUnit.SECONDS); if (locked) { try { json redis.get(key); if (json ! null) { return JSON.parseObject(json, Product.class); } Product dbProduct productMapper.selectById(id); if (dbProduct ! null) { int ttl 1800 RandomUtils.nextInt(600); redis.set(key, JSON.toJSONString(dbProduct), ttl, TimeUnit.SECONDS); localCache.put(id, dbProduct); } else { redis.set(key, NULL, 60, TimeUnit.SECONDS); } return dbProduct; } finally { redis.del(lockKey); } } // 没抢到锁短暂等待后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getProduct(id); }写代码时几个细节值得注意。第一锁的超时时间要远大于业务查询的预期耗时这里给3秒如果数据库查询超过3秒锁自动释放其他线程就能进来重建缓存避免了死锁。第二重建缓存前要做二次查询因为持有锁的线程可能已经把缓存写好了后面的线程没必要再查一次库。第三空值也缓存这是前面说的防止缓存穿透的处理。更新场景下商品信息变更后就执行update db加删除缓存再配合延迟双删。要注意的是删除缓存失败会导致旧数据持续命中所以实际项目里我通常会引入一个简单的重试机制删除失败后把key扔进消息队列消费者再次执行删除。5.3 上线前后的调优过程缓存层上线不是一键搞定就完事需要持续观察指标。我会重点看三个指标的变化。第一个是Redis命中率。上线后每天的keyspace_hits和keyspace_misses要有明确记录命中率目标定在93%以上。如果命中率偏低首先排查的是TTL是否过短、热点集合是否识别齐全然后用redis-cli --hotkeys看看Redis里到底存活的是哪些key和业务热点画像做对比。第二个是数据库回源QPS。从数据库的慢查询日志和监控面板看回源量是否符合预期。如果回源QPS依然很高大概率是某个热点key没有加进缓存或者加了但TTL太短导致了周期性穿透。我遇到过一种情况某个热点商品在详情页有缓存但评论区接口没有缓存每次页面刷新都会回源查一次评论区聚合表直接把数据库打满了。这种问题用热点画像表一对照就能发现。第三个是Redis的慢查询。配置slowlog-log-slower-than 10000单位微秒把超过10毫秒的命令记录下来。慢查询通常意味着大value或者是复杂命令。在商品详情场景里如果某个商品挂了很多图片value可能达到几十KB序列化和网络传输成本都会上去这时就要考虑拆分缓存详情字段和图片字段分开存、分开读。6. 常见问题速查与排查经验6.1 热点key和大key问题热点key和大key是Redis生产环境里最常见的两类隐患而且经常同时存在。热点key的核心问题是单个key的QPS过高把所在节点的CPU和带宽打满。实际排查时如果发现某个Redis节点CPU显著高于其他节点优先怀疑热点key。处理手段有三板斧一是在本地缓存层挡掉一部分流量二是给key做分片比如把product:10086扩展成product:10086-1、product:10086-2等多个副本让请求随机分散到不同key上再在应用侧根据副本号解析真实数据三是走Redis Cluster读写分离让从节点分担读取压力。大key的问题则是数据本身太大导致单个命令的序列化、传输、删除都异常缓慢。比如一个list里存了几百万条元素删除它时Redis会长时间阻塞期间所有命令排队等待。排查大key用redis-cli --bigkeys但要注意在低峰期执行。处理大key不能直接DEL要分批次删比如用hscan遍历hash里的字段一次删一部分list则用ltrim逐步裁剪string类型的大key要考虑拆分或者压缩从源头上把单条value控制在合理范围。6.2 缓存命中率上不去的排查思路命中率是缓存健康状况的体温计。如果连续一周命中率都低于80%我一般按下面这个顺序排查效率最高先确认统计口径是否正确。Redis的INFO stats只统计Redis层面的命中情况。如果你的应用架构里有本地缓存兜底Redis的命中率天生就会偏低因为大量请求被本地缓存挡住了。这种情况本身没问题重点应该看整体回源率。然后检查TTL分布。把缓存key按TTL做一次抽样如果大量key的TTL集中在很短的时间窗口比如全部5分钟那命中率肯定上不去。热点数据的TTL要适当拉长冷数据缩短让TTL的分布本身也符合热度分布。再检查缓存是否被动大量删除。INFO stats里的evicted_keys如果一直在涨说明内存不足淘汰策略在频繁删数据。典型的连锁反应是内存上限不够热点数据刚写入就被淘汰下次请求又回源再写再淘汰命中率自然崩。这时候不是调代码能解决的要扩容或优化value大小。最后看一眼有没有业务逻辑在手动删除缓存。有些团队为了图省事在更新数据库时用keys命令批量匹配删除缓存key这个操作在数据多的时候会全库扫描既慢又会误删大量还没过期的热点数据。我见过不止一次因为这种操作导致缓存命中率掉一半的情况。排查方法是在监控后台搜索删除缓存相关的命令日志。6.3 面试中关于Redis缓存的几道高频题既然说到了Redis缓存顺便整理几个面试中反复出现的考点都是我面人和被面时多次遇到的。“Redis为什么快”答案要点纯内存操作、高效的数据结构、单线程避免上下文切换和锁竞争、IO多路复用。注意不要只答单线程要分层次说。“缓存穿透、击穿、雪崩的区别和解决方案”答案要点穿透查不存在的数据用布隆过滤器或缓存空值击穿是热点key过期瞬间并发回源用互斥锁雪崩是大量key同时过期或Redis整体不可用用随机TTL加高可用架构。“缓存和数据库的一致性怎么保证”答案要点Cache Aside模式、延迟双删、binlog异步删除。要能说出每种方案的优缺点不要只说一个。“缓存容量怎么规划淘汰策略怎么选”答案要点根据热点数据量和value大小推算再结合数据是否可重建选择LFU或LRU。能说出LFU比LRU更适合分布不均匀场景的通常能加不少印象分。“如果你发现Redis节点CPU打满怎么排查”答案要点先看是否存在热点key用redis-cli --hotkeys和MONITOR分析再看是否存在大key--bigkeys检查再看慢查询日志定位具体命令。整个思路就是先定位故障面再缩小到具体key最后采取对应措施。这些考点背后的本质就是一个你是否真的理解缓存是为了解决“热点覆盖”问题而存在的而不是单纯作为数据库前面的一个加速挡板。如果真的动手做过容量规划、调过淘汰策略、排查过命中率问题这些问题很容易答出区分度。最后分享一段我的真实体会。曾经有个项目上线初期大家都没太在意缓存设计统一TTL、默认策略、内存上限拍脑袋定结果高峰期数据库QPS屡屡逼近上限DBA隔三差五就来找我们说慢查询太多。后来花了大概两周时间做了访问日志分析、热点画像、参数调优和代码改造命中率从70%左右稳定到了93%以上数据库回源QPS降了超过一半整个系统瞬间就“宽裕”了。那次之后我深刻意识到缓存设计从来不是“加上就够了”而是要在数据分布、业务特征和运维成本之间找平衡。如果你现在正被缓存问题困扰不要急着加内存先花点时间搞清楚你的20%到底在哪里。