缓存这东西,看起来简单——不就是查一下没有就去数据库拿,再放回缓存嘛。但只要你真正经历过一次线上事故,比如某个热点key过期的一瞬间数据库连接数飙满,或者某个不存在的数据被恶意刷导致接口直接超时,你就会明白,缓存问题并不在于“怎么用”,而在于“用的时候那些细节你有没有想到”。这篇文章是上半部分,我打算把缓存从原理到实践拆开讲清楚,重点落在三个经典事故——穿透、击穿、雪崩,以及数据一致性和淘汰策略的取舍上。不管你是刚接手第一个后端项目,还是已经被线上问题折磨过几轮,这文章都能给你一些可以直接抄走的思路。
1. 先想清楚:缓存到底在解决什么问题
1.1 为什么几乎所有系统都离不开缓存
很多刚入行的同学会把缓存理解为“一个存数据的中间件”,这种理解不能说错,但太浅了。缓存存在的根本理由只有一个字:快。数据库的磁盘IO和CPU计算,在高并发下是扛不住重复压力的,而缓存把高频访问的数据放在内存里,用10倍百倍的速度直接返回,这是性能优化的第一板斧。
我见过不少项目,一上来就堆各种中间件,消息队列、搜索引擎都上,结果发现最基础的接口还是慢。一问才知道,数据库每秒钟被同一个查询打了几百次,热点数据压根没进缓存。这个场景太常见了——很多系统不是被复杂业务拖垮的,而是被重复读拖垮的。缓存就是从根源上砍掉这些“重复劳动”。
从另一个角度讲,缓存本质上是在“时间维度”上做了一次调度:把最近可能用到的数据提前放到更快的存储里,让大部分请求不用触碰后端的数据库。这个思想不止存在于代码里,操作系统的页缓存、CPU的多级缓存,乃至你浏览器里的本地存储,全部是同一个逻辑。理解到这一层,你就知道缓存不是一个“可选项”,而是复杂系统不可避免的组成部分。
1.2 哪些数据适合进缓存,哪些劝你千万别放
但是“能用缓存”不等于“什么都该进缓存”。数据能不能缓存,核心看你是否接受它短暂的不一致,以及它是否真的被频繁读取。
适合放进缓存的,通常是这几类:
- 读取频率远高于写入频率的数据。比如商品详情、用户基本信息、配置项,这些数据读多写少,缓存命中率高,放进去收益非常明显。
- 计算成本高的结果。比如某个复杂的统计报表、聚合了几张表之后的视图数据,每次现算代价大,缓存放一份结果,能省掉大量数据库计算。
- 全局共享的配置类数据。比如系统开关、渠道参数,这类数据变化频率极低,几乎不会产生一致性问题。
不适合进缓存的也有,最典型的是强一致性的数据——账户余额、库存数量、订单状态。这类数据一旦缓存了旧值,轻则用户看到的信息不对,重则会造成超卖或者资金差错。还有一种场景是写入频繁但几乎不怎么读取的,比如日志流水、埋点数据,缓存进去除了白白增加内存压力,没什么收益。最后就是维度特别多的查询,如果每次查询条件都不同,缓存命中率极低,缓存基本形同虚设。
1.3 缓存的两种形态:本地缓存与分布式缓存怎么选
这个是很多人容易忽视的点。缓存从承载位置上分两类:应用进程内的本地缓存,以及独立的分布式缓存(比如Redis)。它们各有利弊,很多项目出了问题,根源就在于混用的时候没搞清楚边界。
本地缓存的优势是极快——没有网络开销,一个Map查找的耗时在纳秒级。但劣势也很明显:首先是多实例之间的数据不一致。假设你部署了4个应用实例,每个实例一份本地缓存,用户请求落到不同实例就拿到不同的数据。其次是内存有限,不会太大,还要和业务代码抢占堆空间,搞不好就是Full GC的隐患。分布式缓存则相反,有网络开销,但数据是集中管理的,所有实例访问同一份,天然一致,容量也大得多。
实际项目中我的选择习惯是:需要跨实例严格一致的场景,一律用Redis;可以做最终一致的场景,比如白名单、静态配置,可以用本地缓存兜一层。很多性能问题其实都是两级配合的——先在本地缓存找,找不到再查Redis,最后才落到数据库。这种结构能在极端热点的时候把压力层层拦截掉。但是注意,两级缓存带来的数据同步复杂度是几何级上升的,后续章节讲到一致性的时候会专门展开。
2. 缓存三大经典事故:穿透、击穿、雪崩
这一节是整个缓存问题的重头戏。很多人知道这三个名字,但真到排查的时候,往往会混淆。我觉得有必要用最直白的方式拆开讲:每个问题的触发场景、底层原因、解决方案,全部说清楚。
2.1 缓存穿透:查询一个不存在的数据,把数据库打穿
缓存穿透说的是,请求的数据在缓存和数据库里都不存在。正常情况下,缓存没命中会回源数据库,数据库也没有,那就不写回缓存,下一次请求来了,还是走同样的流程——“缓存都没命中,数据库查了个寂寞”。如果这种请求量很大,数据库会被这些无意义的查询反复冲击。
最常见的触发场景有两种。第一种是恶意攻击,比如一个接口接收商品ID,攻击者批量构造不存在的ID轮番请求,效果等同于对着数据库做了一次拒绝服务攻击。第二种是业务漏洞。比如前端把参数校验做在了页面里,后端接口没有独立校验,导致乱传参数的请求直接打到数据库。
解决穿透问题,业界常用的手段有以下几种。
方案一:缓存空值与短TTL
既然数据不存在,那就把“不存在”这个结果也缓存起来。查询数据库发现没有,就往缓存里写一个空值标记,比如字符串NULL或者一个特定对象,TTL设置得短一些,30到60秒就够了。这个方案的好处是简单,直接塞在现有代码流程里就能生效。我之前在日志监控里看到过,某一类请求穿透量极大,加了这个措施之后,数据库的无效查询瞬间降了九成以上。
不过有个细节必须注意:空值缓存的时间不能太长,否则数据真的被创建了之后,用户要等很久才能看到。而且如果写入的数据量特别大——比如恶意请求构造了上百万个不同的不存在的ID——那缓存里会塞满空值,白白浪费内存。所以这个方案适合“穿透量可控”的场景,如果量很大,还得配合下面的方案。
方案二:布隆过滤器前置拦截
布隆过滤器是一个很巧妙的数据结构,它可以快速判断一个元素“一定不在集合里”或者“可能在集合里”。用在缓存穿透上,就是在缓存之前加一道过滤:把数据库中存在的ID都放到布隆过滤器里,请求来了先过过滤器,如果判定元素不在,直接返回,根本不查缓存和数据库。它的空间占用极小,上亿个元素也只要几百MB级别,而且查询时间复杂度是O(k),k是哈希函数个数。
但是布隆过滤器有两个代价要心里有数。第一,它只能保证“不在就是不在”,但是“可能在”是有误判率的,也就是说少量不存在的ID可能会穿透布隆过滤器到达缓存层。第二,删除很麻烦,标准布隆过滤器不支持删除,如果业务里ID会出现增删,就需要用Cuckoo Filter或者定期重建。所以布隆过滤器适合数据集合相对稳定的场景,比如用户ID、订单ID这种长期有效的数据。
方案三:接口层基础校验
这个常常被忽略。穿透的本质是非法参数打到了数据库,那你最前面加一道参数校验是成本最低的。比如商品ID必须是正整数,比如时间范围必须合法,先挡住明显不合法的请求。我见过太多项目,外部接口的参数校验形同虚设,所有校验都写在了前端页面里。后端接口裸奔,这本身就是很危险的事。
2.2 缓存击穿:热点key突然过期,一瞬间流量全压到数据库
穿透是“缓存里没有”,击穿则是“缓存里本来有,但刚好那个热点key过期了”。击穿的杀伤力比穿透更猛,因为它打的是热点数据——平时大量请求都依赖这一个key,突然这个key失效了,所有请求在同一瞬间发现缓存不命中,齐刷刷回源数据库,数据库面对的是相当于平时的数倍甚至数十倍流量。
击穿有三个特征值得记住:热点key、突然失效、高并发集中请求。这三者缺一不可。
解决击穿,核心思路是保证“失效瞬间不让过多请求打到数据库”。成熟的方案有三种。
方案一:互斥锁(Mutex Key)
这是最常用的手段。当缓存没有命中时,不是立刻去查数据库,而是先尝试获取一个分布式锁。只有拿到锁的请求才允许去查数据库,其他请求在原地等待一段时间后重新查询缓存。第一个请求查到数据后写回缓存,后续请求再从缓存里拿到结果。
用Redis实现时,常见的做法是用SET key value NX EX timeout获取锁,查完库之后删除锁。这里有一个坑:锁的过期时间要大于数据库查询的耗时,否则第一个请求还没查完锁就自动释放了,其他请求又会重复回源。我习惯的做法是把锁过期时间设成1到3秒,同时查询数据库的语句加上超时控制,宁可让请求失败,也不能让多个请求同时压到数据库。
// 伪代码:互斥锁方案 public Object getData(String key) { Object data = redis.get(key); if (data != null) { return data; } // 尝试获取锁,timeout 为锁的过期时间 boolean lock = redis.set("lock:" + key, "1", "NX", "EX", 3); if (lock) { try { data = dao.query(key); redis.set(key, data, 300); return data; } finally { redis.delete("lock:" + key); } } // 没拿到锁,短暂等待后重试 Thread.sleep(50); return getData(key); }锁方案的缺点就是会有少量请求等待,增加了平均响应时间,但对于保护数据库来说,这个代价非常值。
方案二:热点key逻辑过期
这个方法有点取巧但实战中效果极好。不给热点key设置固定的过期时间,而是在value里额外存一个过期时间戳。读取时如果时间戳显示已经过期,不直接删除key,而是先返回旧值,同时后台异步去更新缓存。
// 伪代码:逻辑过期方案 public Object getData(String key) { CacheItem item = redis.get(key); if (item == null) { // 缓存不存在,走互斥锁查库流程 return loadFromDb(key); } if (item.expireAt > System.currentTimeMillis()) { // 逻辑上未过期,直接返回 return item.value; } // 逻辑上已过期,触发异步更新,但旧的value还能直接返回 asyncRefresh(key); return item.value; }为什么说它效果好?因为基于互斥锁的方案在缓存失效瞬间会有短暂的空窗期,部分请求还是要在锁上等待的。而逻辑过期方案则完全不存在空窗期,所有请求都能第一时间拿到旧值,由后台线程去刷新缓存。这个方案唯一的坏处是实现起来较复杂,而且缓存中的数据可能短暂不新鲜,不过对于绝大多数场景,短时间读到旧值是可以接受的。
方案三:热点key永不失效 + 定时刷新
最简单粗暴的做法就是把热点key的TTL设置成永不过期,然后通过定时任务每几分钟主动更新一次。这个方案可以理解成“人工管理热点key的生命周期”,对运维要求高一些。需要你维护一张热点key清单,并且能在业务变化时及时调整,不然容易出现缓存雪崩。我的建议是,除非热点key的列表非常稳定,否则还是用别的方案更省心。
2.3 缓存雪崩:大量key同时失效,数据库直接被压垮
击穿是“一个热点key失效”,雪崩则是“一大批key在同一时间点失效”。两者的解决方案有共通之处,但雪崩的影响面更广,需要从设计层面规避。
造成雪崩的原因通常是两个。一个是物理性的,比如Redis集群挂了,所有请求瞬间全部落到数据库。另一个是逻辑性的,比如给大量key设置了相同的过期时间,这些key在同一秒集体过期,流量波峰直接打满数据库。
解决雪崩,从设计角度看可以做几件事。
办法一:过期时间加随机扰动
给key的TTL加一个随机偏移量,让过期时间分布的离散程度变大。比如原来所有key都是10分钟过期,现在改成8到12分钟之间随机。这样即使同一批数据被同时写入缓存,它们也不会在同一时间集体过期,数据库收到的回源流量会被摊平。
// TTL加随机偏移 int baseTtl = 600; // 基础10分钟 int randomOffset = new Random().nextInt(120); // 0~120秒随机 redis.set(key, value, baseTtl + randomOffset);办法二:Redis高可用与多级缓存兜底
如果是Redis集群挂了导致雪崩,那必须从架构层面解决。主要的做法是Redis主从加哨兵,或者使用Redis Cluster模式,保证单点故障时依然可以对外提供服务。同时可以在前面再挡一层本地缓存,Redis挂了的时候,本地缓存依然能扛住一部分热点请求,给故障恢复争取时间。
我自己经历过一次Redis节点故障导致的全站雪崩,那次真的是头皮发麻——数据库压力瞬间翻了三倍,接口超时率飙升。事后复盘,根因就是架构上只有单节点Redis,且完全没有本地缓存兜底。把这些架构补齐之后,类似故障的影响面小了一个数量级。
办法三:服务降级与限流
最后一个兜底手段是让系统在异常时“有尊严地失败”。当发现数据库压力濒临极限时,直接对非核心接口返回降级内容,或者对核心接口做限流处理,把一部分流量拒之门外。这比数据库被拖垮之后整个系统不可用要好得多。
2.4 三个问题的横向对比与处理优先级
这三个问题容易被混为一谈,我整理一个对比表,方便你在排查时快速对应。
| 问题 | 触发场景 | 危害表现 | 核心思路 | 常见方案 |
|---|---|---|---|---|
| 缓存穿透 | 数据不存在,缓存和数据库都没有 | 大量无效请求打到数据库 | 拦截不存在的请求 | 空值缓存、布隆过滤器、参数校验 |
| 缓存击穿 | 热点key突然过期 | 瞬时流量全部压到数据库 | 失效瞬间只允许少量请求回源 | 互斥锁、逻辑过期、key永不过期 |
| 缓存雪崩 | 大量key同时失效或Redis不可用 | 数据库压力瞬间陡增 | 让失效时间离散化,架构兜底 | TTL随机、多级缓存、限流降级 |
如果线上同时出现多种异常,我建议先解决穿透,再解决击穿,最后处理雪崩。原因是穿透的代码改动最少,一个空值缓存就能立即止血。击穿需要加锁逻辑,改动量中等。雪崩牵涉架构调整,需要在系统稳定下来之后再动手。
3. 缓存与数据库的数据一致性
缓存用上了,接下来的问题逃不掉:缓存里的数据和数据库里的数据怎么保证一致。说实话,想要做到强一致,缓存这个方案本身就不合适。你能做的是选择合适的策略,把不一致窗口缩小到业务可以接受的程度。
3.1 不一致是怎么产生的:先更新库还是先删缓存
数据更新时,你有两个操作可以做:更新数据库,更新缓存或者删除缓存。两个操作放在不同顺序,会带来完全不一样的后果。
先更新数据库,再更新缓存
这个方案听起来顺理成章,但有两个问题。一是并发写的时候容易产生脏数据:请求A先把数据库更新为值1,然后请求B把数据库更新为值2,结果B先把缓存更新成2,A再更新成1,缓存里就留下了旧值。二是如果缓存更新失败,数据库和缓存直接不一致。
先更新数据库,再删除缓存
这是比较推荐的方案。删除缓存比更新缓存安全,因为下次读取时缓存未命中,会重新从数据库加载最新值,天然保证了最终一致。但删除操作依然可能失败,删不掉的缓存会一直保留旧值。针对这个问题,可以引入可靠消息或者订阅数据库变更日志来异步重试删除。
先删除缓存,再更新数据库
这个顺序问题最大。请求A先删缓存,还没来得及更新数据库,请求B就来了,发现缓存没命中,从数据库读到旧值,再写回缓存。结果请求A更新了数据库,缓存里却躺着一个旧值,而且这个旧值在TTL过长时会持续很久。所以这个顺序强烈不推荐。
3.2 主流方案对比:Cache Aside、延迟双删、订阅binlog
Cache Aside(旁路缓存模式)
这是最经典的读缓存模式,几乎所有教材都会讲。读的时候先查缓存,不中再查库,最后写回缓存。写的时候先更新数据库,再删除缓存。它保证的是最终一致性,但是有一个小窗口期:在“数据库已更新、缓存已删除”之间,如果有读请求,它可能从缓存里拿到旧值。
延迟双删
为了解决上面那个窗口期,有人提出“延迟双删”方案:先删除缓存,再更新数据库,隔几百毫秒再次删除缓存。为什么第二次删除能解决问题?因为第一次删除后到第二次删除之间的那个时间窗口里,如果正好有请求把旧值写回了缓存,第二次删除会把旧值再次清掉。这个方案比简单的Cache Aside更稳,但是要控制好延迟时间,通常设置500毫秒到1秒。缺点是你需要额外维护一个延迟队列或者定时任务,复杂度上去了。
补充一个实战细节:这个延迟时间到底设多少,没有标准答案。我的建议是根据你数据库主从复制的延迟和业务特性来定,先打日志看下实际场景里“旧值写回缓存”的时间分布,再来调整,不要拍脑袋定1秒或者2秒。
订阅数据库binlog异步删除
这是把一致性兜底做到最彻底的方式。应用在更新数据库之后正常返回,数据库的变更会被记录到binlog(比如使用一个中间件来模拟从库,解析binlog),然后由异步程序把变更解析出来,去删除对应的缓存。这个方案的好处是逻辑简单、入侵性小,完全不依赖业务代码里“删除缓存是否成功”,坏处是需要额外部署一套解析同步的组件,运维成本高。
从实操角度看,中小型系统用Cache Aside加一个删除失败重试,已经能覆盖绝大多数场景。只有对一致性要求极高的核心链路,才值得引入订阅binlog重放删缓存。
这里还要点名一个经典误区:有人为了强一致,把“更新缓存”和“更新数据库”包在同一个事务里。先说结论,分布式事务的开销极大,绝大多数业务根本用不上。缓存本来就是一个允许坎肩最终一致的组件,你的IT系统需要的是“在合理时间内收敛到一致”,而不是“任何时刻都绝对一致”。
4. 缓存淘汰策略与容量规划
缓存不是无限大的,内存就那么多,所以你必须想清楚一件事:当缓存满了,谁该被请出去。
4.1 TTL、LRU、LFU怎么选
TTL(过期时间)
这是最简单粗暴的淘汰方式,设置一个过期时间,时间到了数据自动失效。它的缺点是“被动淘汰”属性强,如果一个key从来不被访问但它一直没到过期时间,就会一直占着内存。所以TTL一般不会单独用,都是和其他淘汰策略配合。
LRU(最近最少使用)
核心思想是:如果一个数据最近被访问过,那未来被访问的概率也高;如果它很久没被访问了,那说明它大概率已经没人需要了。所以淘汰时优先淘汰最久没被访问的数据。这是应用最广泛的策略,大部分缓存框架的默认选项就是它。比如本地缓存库Guava的默认淘汰策略就是基于LRU思想。
但LRU有一个典型的场景缺陷,就是“缓存污染”:比如某些大促活动产生的数据只在短时间内被高频访问,之后没人再查,但LRU在它被访问时把它标记成了“最活跃”,于是在很长一段时间里它都赖在缓存里不肯走,挤走了真正有用的数据。
LFU(最不经常使用)
LFU的核心是统计访问频率,淘汰频率最低的。它能更好地抵抗缓存污染,但实现复杂度更高,需要额外记录每个数据的访问次数,而且“冷启动”时会有一个积累期,大量新写入的数据一开始频率都是0,容易被误淘汰。Redis的allkeys-lfu策略就属于这一类。
实际选择上,我的经验是:通用场景直接上LRU,有问题再调。如果访问模式确实有明显的“短时热点”特点,比如秒杀商品、刷屏内容,再考虑LFU或者带衰减因子的升级版LRU。有一个折中方案是LRU加一个“保护时间窗口”,磁盘数据刚被读取时不参与淘汰,过一段时间再看它的实际活跃度,这能有效缓解缓存污染。
4.2 热点数据识别与缓存预热
有些数据注定是热点,提前放进去总比等请求来了再慢慢灌好。这就是缓存预热。
预热最典型的场景是:系统刚启动、大促活动开始前、定时任务每天早上处理之前。做法很简单,写一个启动或预热任务,提前把这些数据的额叶刷进缓存。比如商品系统每次活动前,把活动商品、秒杀商品的详情缓存进去,同时在活动开始前把关键key的TTL设置得长一些。
热点数据识别的另外一个办法是分析访问日志。从网关或者应用层把请求的key记下来,做聚合统计,找出QPS最高的那一批,把它们标记为热点key,配置单独的预热策略或者更长的过期时间。这个工作可以做成定时任务,每5到10分钟更新一次热点清单。
我之前做过一个在线教育的课件系统,视频播放接口的响应时间一度漂到了好几秒。排查后定位是课件元数据每次请求都现查数据库。做了一轮缓存预热,把当天访问最多的1000个课件提前刷入Redis,接口响应直接掉到两位数毫秒。缓存预热在很多场景下比调代码的性能收益还要夸张。
4.3 容量估算:别把内存不当钱
缓存容量规划的核心公式并不复杂:你需要缓存的数据量 × 平均对象大小 × 预估的膨胀系数。比如你要缓存10万个商品详情,每个对象的JSON约2KB,膨胀系数按1.5算,大约就是300MB。如果Redis跑在8GB的机器上,这个容量没问题,但如果你想缓存1000万个用户信息,每个50字节,那加起来就是750MB,还在可控范围。
但有一个容易忽视的点:Redis不只是存你的业务数据,它还有自己的内部结构开销。每个key本身约几十字节的元数据,如果key数量特别多,这部分开销会非常可观。我之前在线上遇到过一个小key存储几十万个的场景,光key自身的元数据就占了好几十MB。
所以做容量规划的时候,不要只盯着“业务数据大小”,要把key数量和内部碎片也算进去。预留至少30%的空余内存,用来应对流量峰值和内存碎片。如果内存不够,先想想能不能通过压缩value(比如用MessagePack代替JSON),再考虑扩容。
5. 线上问题排查思路实录
前面讲了很多理论和方案,最后这一节放点实际的——线上系统出了问题,你从哪儿下手、怎么判断它到底是不是缓存引起的。
5.1 怎么判断当前是不是缓存问题
很多同学一看到接口快、一看到数据库压力大,就脱口而出“加缓存”,实际上问题的根因可能根本不是缓存。我习惯用一个三步排查法来确定问题的性质。
第一步,看响应时间分布。如果接口响应时间呈现明显的“两极分化”,也就是说大部分请求很快,偶尔有几个请求特别慢,那大概率是缓存命中率不够高或者缓存失效导致的回源。如果所有请求都慢,均匀变慢,那可能是数据库整体负载高,或者网络、连接池问题。
第二步,看Redis的监控指标,特别是命中率。命中率掉到90%以下,就要警惕了。命中率掉到50%以下,基本可以断定业务逻辑里缓存的使用方式有严重问题。
第三步,看数据库的慢查询日志。如果慢查询集中在某几张表,而且查询条件跟缓存key的维度一致,那基本可以确定是缓存失效导致回源。
5.2 一次典型的缓存穿透排查案例
很久以前我负责过一个活动报名系统。上线第二天,运营反馈说用户访问特别卡,数据库CPU直接飙到95%以上。我当时的第一反应是SQL慢查询,就先查了慢日志,结果发现有一条SQL的调用次数异常高,QPS过千,而且查的都是不存在的活动ID。
排查过程是这样的:先在Redis查这些活动ID对应的key,发现全部为空。再查数据库,发现数据表中确实没有这些ID。这就指向了缓存穿透——前端调用时传入的活动ID在数据库中根本不存在,导致每次请求都打到了MySQL。
当时的修复动作做了两件事。第一,在活动查询接口加了参数校验,严格要求活动ID必须为正整数且不能超过合理范围。第二,在查询结果为空时缓存空值,TTL设了60秒。上线之后,数据库压力立刻降下来了,穿透QPS从千级别降到个位数。
这次事故给我的教训是:上线前一定要把接口的参数校验和边界情况考虑完整,同时缓存空值这种防御性代码,不是可有可无的锦上添花,而是高并发系统的底线操作。
5.3 监控指标与报警阈值建议
最后把和缓存相关的重要监控指标整理出来。如果你们公司监控系统里有这些指标,一定要配好报警;如果还没有,值得尽快补上。
| 指标 | 含义 | 建议报警阈值 |
|---|---|---|
| Redis命中率 | 缓存命中的请求占总请求比例 | 低于90%告警,低于80%严重告警 |
| 数据库QPS | 回源数据库的每秒请求数 | 超过日常基线2倍时告警 |
| Redis内存使用率 | 已用内存占总内存比例 | 超过80%告警,超过90%严重告警 |
| 缓存key数量 | 当前缓存中key总量 | 出现异常陡增时告警 |
| 慢查询数量 | Redis或数据库中的慢查询次数 | 单分钟超过30次告警 |
监控的价值在于把“事后救火”变成“事前预警”。缓存穿透这种事,如果数据库QPS的告警早几分钟触发,完全可以避免演变成用户大面积卡顿。
我个人的体会是,缓存问题看起来是一个技术问题,本质上考验的是你对业务访问模式的理解。同一个缓存方案,在一个业务里是神药,在另一个业务里可能反而制造出更大的问题。所以不要死记方案,而是要真正搞清楚每个方案背后的权衡——它解决了什么问题,又引入了什么新的问题。下一篇文章我会把缓存键设计、分布式缓存与本地缓存混用、以及缓存全链路追踪这些更细的内容展开,到时候可以再接着看。