十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从单机到分布式:缓存失效、击穿与一致性实战解析

从单机到分布式:缓存失效、击穿与一致性实战解析 先给各位同行看一个我印象特别深的监控截图某个周五晚高峰缓存命中率从99.2%直接掉到37%随后数据库CPU在五分钟内拉满慢查询队列瞬间堆了几百条。事后复盘就一行改动引发的——一个热点key过期了而代码里对这个key既没有做互斥重建也没有设置热点保护。从那之后我开始认真思考一个一直想当然的问题单机缓存到底在什么情况下会缓存失效失效之后系统会发生什么以及当我们把缓存从单机搬到分布式缓存能否从根本上解决这个问题。答案显然是否定的——分布式缓存能解决单机的容量和可用性问题但会把缓存一致性问题放大到整个集群层面。这中间有非常多的工程细节不是背几个缓存读写套路就能搞定的。这篇随笔就沿着我自己的实践路径来写从单机缓存的失效场景开始讲到分布式缓存集群的落地再到缓存一致性体系的构建最后聊聊我做多语言项目时对缓存代码语法层面的思考。内容偏实践适合正在做后端开发、系统架构或者准备梳理缓存体系的工程师参考。1. 单机缓存为什么明明加了缓存系统还是扛不住1.1 单机缓存失效的三类典型场景先说清楚一个概念单机缓存失效不是只有“缓存项过期”这一种情况。我习惯把它拆成三类缓存穿透、缓存击穿、缓存雪崩。这三兄弟经常被混淆但成因、现象和解决方案完全不一样。缓存穿透是指查询一个根本不存在的数据缓存里没有数据库里也没有每次请求都要打到数据库。这种请求就像穿透了缓存层一样绕过所有保护直接压向存储层。攻击者如果构造一批不存在的ID去循环请求数据库基本是秒挂。缓存击穿是指某个热点key在过期的一瞬间大量并发请求同时发现缓存没命中全部去数据库重建缓存。这跟穿透的区别在于击穿的时候数据是存在的只是因为过期的瞬间没有缓存兜底导致流量集中打到数据库。缓存雪崩则是指大量key在同一时间段集中过期比如缓存预热时统一设置了相同的过期时间或者缓存服务整体宕机导致所有请求全部落到数据库。雪崩的影响范围最大往往是系统性的。单机缓存的失效场景还不止这三种。进程内缓存比如Caffeine、Guava Cache还要考虑JVM内存上限、GC停顿导致的缓存抖动、进程重启带来的一次性全量失效。这些在单机时代还能靠“重启大法”和“堆内存”糊弄过去一旦流量上来就全部暴露了。1.2 从我踩过的坑说起一次缓存失效引发的线上事故那次事故我至今记忆犹新。业务上有个“今日热榜”的接口QPS常年保持在两万左右。当时的实现是用一个定时任务每分钟把热榜数据写入本地Caffeine缓存key的过期时间设置为60秒逻辑上看起来没什么问题。问题是出在定时任务执行失败的时候。定时任务线程池因为一次Full GC停顿没有在预期时间内完成数据刷新而Caffeine缓存里的旧数据又因为到达60秒过期时间被清掉了。这个窗口期里所有请求都去查数据库数据库的连接池只有50个连接一下就满了。后续请求全部超时上游服务开始重试流量又叠加了一倍最终把整个服务链路拖垮。复盘的时候我发现问题不只是“缓存过期”这么简单而是缺乏对缓存失效的系统性防御。我们当时的处理方式是把缓存过期时间加长到5分钟对定时任务加了超时保护。但这是治标不治本因为单机缓存天生就有这个弱点——缓存与数据源之间没有一致性保障机制一旦刷新链路出问题缓存就是一个不设防的膜。后来我养成了一个习惯不管用什么缓存方案先问自己三个问题。缓存失效了谁来重建重建期间请求怎么处理重建失败了有没有降级方案这三个问题想清楚单机缓存才不会成为系统的单点。1.3 基础防护手段空值缓存、互斥锁与热点保护针对三类失效场景有一些教科书级的防御手段但实际落地时有很多细节需要琢磨。缓存穿透的经典解法是空值缓存就是把“查不到”这个结果也缓存起来过期时间设置短一些比如60秒。这个方案的问题是如果攻击者构造大量随机的不存在ID空值缓存会占用大量内存。我做过的项目里会加一个布隆过滤器前置拦截把所有合法ID先过滤一遍。布隆过滤器有误判率我一般控制在1%以内宁可让它放过几个无效请求也不能把有效请求误杀。缓存击穿的经典解法是互斥锁也就是只有一个线程能去数据库重建缓存其他线程等待。用Redis做分布式锁的时候要注意锁的粒度要按key来不能搞一个全局锁否则不同key的缓存重建会互相阻塞。另外等待的线程要有超时机制不能无限等下去。还有一个非常实用的手段是热点key永不过期或者叫逻辑过期。就是缓存里不设置物理过期时间而是存一个逻辑过期时间戳。读取的时候判断时间戳是否过期过期了就异步去刷新缓存请求本身仍然返回旧数据。这个方案能从根本上消除击穿问题代价是数据会有一段短暂的不一致但对很多场景比如热门榜单、配置信息完全够用。缓存雪崩的防护思路是错峰过期和熔断降级。错峰过期很简单给过期时间加一个随机扰动比如3600秒加上0到300秒的随机数避免大量key同时失效。熔断降级则是在数据库压力过大时直接返回旧缓存或者默认值保证核心链路不被打垮。这些手段在单机缓存阶段就够用了。但问题是单机缓存本身有一个绕不开的天花板数据只能存在一台机器上容量有限可用性有限。这就是我们走向分布式缓存的根本原因。2. 走向分布式缓存从Redis单节点到集群的必经之路2.1 单机Redis的局限性内存、并发与可用性很多人觉得单机Redis已经很快了单线程模型、10万QPS为什么还要上分布式缓存我举一个真实例子。某个用户画像服务需要缓存全量用户的标签数据大约2亿个key每个key平均2KB加起来就是400GB内存。单机Redis根本装不下。就算你有一台大内存机器单点故障问题也躲不掉。Redis进程宕机、宿主机断电、网络分区任何一个故障都会让整个缓存层不可用。更麻烦的是Redis持久化在AOF rewrite或者RDB bgsave的时候会fork子进程内存越大fork耗时越长短则几百毫秒长则几秒这个期间缓存读写延迟会明显上升。单机Redis还有一个并发层面的问题虽然Redis本身是单线程处理命令但客户端连接数和网络带宽是有上限的。当单key的value特别大比如几百KB或者执行慢命令比如KEYS、HGETALL大哈希整个实例都会被拖慢。你把所有鸡蛋放在一个篮子里篮子一旦出问题影响就是全局性的。所以分布式缓存的第一个动机就是把数据分散到多台机器上每台机器只承担一部分数据这样容量、并发和可用性都能线性扩展。但“分散”这件事听起来简单做起来全是细节。2.2 分布式缓存的技术选型一致性哈希与虚拟节点做数据分散最简单的方式是哈希取模hash(key) % NN是节点数量。这个方案的缺点是N一变化几乎所有的key都会重新映射导致大规模的缓存失效——这正是我们前面讲的雪崩的另一种形态扩容或缩容引发的缓存集体失效。一致性哈希就是来解决这个问题的。它的核心思想是把哈希值空间组织成一个环形结构节点也哈希后放到环上key哈希后顺时针找第一个节点就把这个key映射到该节点。当节点增加或减少时只有环上该节点附近的一小段key需要重新映射其他key不受影响。但基础版一致性哈希有一个问题——数据倾斜。节点少的时候哈希环上的分布可能很不均匀有的节点负载高有的节点负载低。解决方案是引入虚拟节点一个物理节点在环上对应多个虚拟节点通常是100到200个让节点在环上的分布更均匀。我在生产环境里的实际选择是优先用Redis Cluster而不是自己实现一致性哈希。原因有几点Redis Cluster内置了哈希槽16384个槽的概念每个key通过CRC16计算后映射到一个槽槽在节点间迁移时可以做到在线扩容缩容客户端比如Jedis、Lettuce、go-redis对Cluster协议支持成熟不用自己处理重定向它还有主从复制和故障自动转移。自己做一致性哈希的优势是灵活但维护成本高适合有特殊需求的团队。2.3 分布式缓存不等于分布式一致性这是我在实践中最想强调的一点你把缓存从单机变成集群解决的是容量、并发和可用性问题但一致性问题的复杂度反而上升了。举个典型例子单机缓存里一个key只有一个副本读写都在同一台机器上不存在副本间数据不一致的问题。但在Redis Cluster里一个key有主从两个副本主节点负责写从节点负责读。如果主节点写入后从节点还没来得及同步就宕机了那么请求打到从节点上就会读取到旧数据。这就是分布式缓存引入的第一个一致性挑战主从副本之间的数据一致性问题。更麻烦的是分布式环境下没有全局时钟跨节点的数据更新顺序变得难以确定。你在A节点更新了缓存在B节点也更新了缓存但两次更新的到达顺序不同最终缓存里的值可能是旧的。这涉及分布式系统里经典的顺序一致性、最终一致性等概念。做缓存体系的时候必须把一致性的目标定清楚是强一致还是最终一致还是允许一定程度的不一致。以我经手的项目为例大部分业务场景选择的是最终一致性。也就是数据写入数据库成功后通过一定的机制异步更新缓存允许缓存有短暂的时间滞后但最终要达到与数据库一致的状态。明确一致性的边界和目标比纠结“怎么保证绝对一致”要实际得多。3. 缓存一致性体系缓存与数据库的最终一致3.1 缓存一致性问题的本质缓存与数据库的一致性本质上是两个存储系统之间数据同步的问题。数据库是事实来源缓存是加速层两者之间存在复制延迟。这个延迟就是不一致的窗口。要理解缓存一致性就必须先回答三个问题什么时候更新缓存用什么机制更新缓存更新失败了怎么办。缓存更新的时机有两种策略。一种是先更新数据库再更新缓存另一种是先删除缓存再更新数据库。这两种策略在并发场景下各有风险。先更新数据库再更新缓存如果两个请求并发执行可能出现后更新的请求先写缓存导致缓存里存的是旧值。先删除缓存再更新数据库则存在一个窗口期缓存为空请求会打到数据库但这个窗口通常很短。我这里的结论是在绝大多数业务场景下优先选择“先更新数据库再删除缓存”也就是Cache Aside模式的写路径。为什么是删除缓存而不是更新缓存因为更新缓存需要知道新值而很多时候写数据库和写缓存的值并不一一对应比如缓存里存的是经过聚合计算的数据删除缓存让下次读取时重建逻辑更简单也更不容易出错。3.2 三种经典的缓存一致性方案缓存与数据库的读写方案业界有几种经典模式每种模式的适用场景不一样。Cache Aside是应用最广泛的模式。读路径先读缓存命中直接返回未命中则读数据库写入缓存再返回。写路径先更新数据库再删除缓存。这个模式的优点是实现简单读多写少的场景下性能很好。缺点是删除缓存后到缓存重建之间存在空窗期期间所有请求都会打到数据库。Read Through和Write Through是把缓存代理层做厚应用层不用关心缓存的具体读写逻辑只跟缓存代理交互。代理层负责在缓存未命中时加载数据在写数据时先写缓存再写数据库。这种模式对应用层很友好但代理层的实现复杂度高一般由缓存中间件提供比如Redis的RedisJSON模块在某些场景下可以做类似的事。Write Behind异步写回是把所有写操作先写入缓存然后异步批量写回数据库。这种模式写入延迟最低但存在数据丢失风险——如果缓存宕机未写回数据库的数据就丢了。一般用在写多读少、允许少量数据丢失的统计类场景比如计数器和日志收集。三种方案的对比我整理了一张表方案读性能写延迟一致性复杂度适用场景Cache Aside高取决于数据库最终一致低读多写少常规业务Read/Write Through高取决于缓存代理最终一致中需要缓存代理层透明化Write Behind高极低最终一致可能丢数据中写多读少容忍少量丢失3.3 延迟双删的实操细节Cache Aside模式看着简单但有一个经典并发问题线程A更新数据库为值1删除缓存线程B更新数据库为值2也删除缓存。如果线程A先删缓存线程B在A删除缓存之前读到了旧缓存值就会把旧值写回缓存。这就是缓存与数据库不一致的一种典型场景。业界常用的解法是延迟双删先删除缓存再更新数据库隔一段时间再次删除缓存。第一次删除是为了让后续读请求尽量走数据库拿到新值第二次删除是为了兜底把第一次删除到数据库更新完成之间可能被写入的旧缓存值清掉。延迟时间怎么定我一般用公式延迟时间 一次读请求的平均耗时 一次写请求的耗时余量。如果平均读耗时是20ms写耗时是10ms那延迟时间至少设置到50ms以上实际生产环境我常设到300到500ms宁可多等一会也不能因为时间太短导致第二次删除发生在旧值回填之后。延迟双删还有一个常见坑第二次删除可能失败。如果第一次和第二次删除都没问题但第二次删除因为网络异常失败了缓存里就可能残留旧值。我的做法是引入删除失败重试机制用消息队列把删除任务发给一个独立的消费者消费者删除失败后自动重试直到成功。这个兜底机制我称之为“删除保障队列”是延迟双删方案里必不可少的一部分。3.4 从秒杀场景看一致性体系的构建秒杀场景是缓存一致性要求最极端的例子。库存扣减如果做不好超卖用户能利用并发漏洞把库存刷爆。我的做法是分三层一致性保障第一层是预热。秒杀开始前把库存提前写入Redis并且设置一个足够长的过期时间。过期时间必须覆盖整个秒杀活动周期否则活动进行中库存key过期就会出现“还有库存但无法下单”的问题。第二层是扣减。用Redis的Lua脚本原子性地执行“判断库存充足-扣减库存”两个操作避免多个请求同时读到相同的库存值。Lua脚本的原子性在单机Redis上是可靠的在Cluster模式下需要确保同一个key的所有操作路由到同一个节点Redis Cluster本身就保证了这一点。第三层是对账。缓存扣减成功只代表“预扣成功”真正的订单创建和数据库扣减是异步进行的。如果数据库扣减失败要通过消息队列把Redis里的库存加回去。这个对账机制保证了缓存和数据库之间即使出现短暂不一致也可以通过补偿操作最终收敛到一致状态。这套体系的核心思路是缓存层负责扛住高并发流量数据库层负责保证最终事实正确两者之间通过异步对账和补偿机制来收敛一致性。4. 多语言语法思考同一套缓存逻辑怎么用不同语言表达才优雅4.1 Java生态注解缓存与Lua脚本的取舍Java后端最常见的缓存做法是Spring Cache注解Cacheable加在方法上框架自动处理缓存读写。这个方案在简单场景下非常爽但有几个隐藏坑一是方法的入参必须能正确生成缓存key如果入参是个复杂对象默认的key生成策略会产生很长的key甚至报错二是被注解的方法不能是private方法不能是同类内部调用否则Spring AOP代理不生效注解就成了摆设三是没有内置的互斥锁机制热点key击穿问题在注解模式下很难透明解决。所以我在核心链路上基本不用注解缓存而是直接用RedisTemplate或者Lettuce手写缓存逻辑。手写的好处是心智可控可以对key设计、过期时间、重建逻辑做精细控制。做复杂查询缓存时我会把整个查询逻辑封装成一个函数用分布式锁包裹锁内先查一遍缓存双检锁未命中才查库回填。涉及到原子性操作时我的选择是Lua脚本。比如库存扣减、限流计数、分布式锁的释放这些操作要求“判断更新”作为一个原子单元执行。Redis的Lua脚本可以保证在单个Redis实例上原子执行比用多个命令在客户端拼装要安全得多。但Lua脚本也有维护成本脚本逻辑一多排错和调试都麻烦。我的习惯是给每个脚本写独立的单元测试用嵌入式Redis跑真实实例去验证脚本行为。4.2 Go语言代码即文档的并发原语Go语言做缓存逻辑的时候我最喜欢它的一点是标准库自带golang.org/x/sync/singleflight这个库简直是缓存击穿的天然克星。singleflight的原理是多个goroutine同时请求同一个key时函数执行一次结果被所有goroutine共享。对于缓存重建场景这意味着同一个key的并发缓存未命中只会触发一次数据库查询其他goroutine等待结果即可。Java里要实现类似效果得用JVM级别的锁或者引入第三方库Go用单飞库几行代码就搞定了。Go的context机制也让缓存调用链路的超时控制和链路追踪变得很优雅。我可以在缓存读取时设置一个短超时比如50ms超时后自动降级为查数据库同时把这次降级记录到日志里。context的超时传递可以贯穿整个缓存读写链路这在Java里需要手动传递参数或者用ThreadLocal语法层面上臃肿很多。另一个我在Go项目里常用的点是把缓存操作封装成泛型函数。Go 1.18之后支持泛型我可以写一个CacheGet[T any]函数传入key、加载函数和过期时间返回对应的类型。这种写法在Java里就是模板方法模式加上泛型在Go里直接一个函数搞定代码量直接减半。4.3 Python与Node.js从装饰器到异步并发Python做缓存最优雅的语法是装饰器。我可以自定义一个装饰器把缓存读写的逻辑从业务函数中剥离出来。一个简单的做法是def cache(key_prefix, expire60): def decorator(func): wraps(func) def wrapper(*args, **kwargs): key f{key_prefix}:{args}:{kwargs} value redis.get(key) if value is not None: return pickle.loads(value) value func(*args, **kwargs) redis.setex(key, expire, pickle.dumps(value)) return value return wrapper return decorator这个装饰器开箱即用但pickle反序列化有安全隐患生产环境我会换成JSON或者msgpack还要处理函数参数里有不可哈希对象的情况。Python的GIL在并发场景下是个限制做缓存重建的互斥锁时我一般用Redis分布式锁而不是线程锁因为可能会有多进程同时运行同一个服务。Node.js的异步模型在处理缓存的并发重建时有独特的优势但也容易踩回调地狱。用async/await重写之后缓存读写的语义才变得清晰。我自己在Node.js项目里的做法是写一个withCache工具函数async function withCache(key, loader, { ttl 60, stale false } {}) { const value await redis.get(key); if (value ! null) return JSON.parse(value); if (stale) return loader(); // 允许过期时直接回源 const mutexKey ${key}:lock; const lock await redis.set(mutexKey, 1, EX, 5, NX); if (!lock) { const wait await fallback(key); if (wait ! null) return JSON.parse(wait); } try { const fresh await loader(); await redis.setex(key, ttl, JSON.stringify(fresh)); return fresh; } finally { await redis.del(mutexKey); } }这个函数把常规缓存读取、热点击穿保护、锁等待降级都封装在一起每个项目的缓存代码风格非常统一。语言本身没有绝对的优劣关键是找到匹配语言习惯的表达方式。4.4 多语言共存的工程实践统一序列化与跨语言调试真正的大型互联网项目里不同服务用不同语言是常态。Java写交易核心Go写网关BFFPython写数据分析Node.js写中台每个服务都要访问同一套Redis缓存。这时候最大的坑就是序列化协议不统一。Java的JDK序列化、Python的pickle、Go的encoding/gob互相之间完全不能读。一旦两个服务之间通过缓存传递数据序列化格式必须统一。我的团队最终定了两个原则缓存value一律用JSON或者MessagePack禁止使用语言原生的序列化器key的规范统一为“业务域:功能模块:ID”不能有空格和中文字符。跨语言调试缓存问题时最痛苦的是key的编码和过期时间不一致。后来我们给Redis加了一层命令行工具封装统一用redis-cli配合一段脚本去扫描特定前缀的key同时把ttl也打印出来排查效率提高了不少。另一个容易忽略的问题是监控埋点。不同语言的Redis客户端对慢查询、连接池状态的暴露方式不一样Java的Lettuce有Micrometer集成Go的go-redis有自己的Hook接口Python的redis-py则比较朴素。我的做法是在缓存客户端外层统一包装一层自己的拦截器打印耗时和命中率日志格式统一输出到日志系统再通过Prometheus汇总展示。这样不管上层是什么语言底层缓存的可观测性是一致的。5. 常见问题与排查技巧实录5.1 缓存不一致的典型场景速查我在多个项目里梳理过缓存不一致的常见场景整理成了一张排查速查表现象可能原因排查方向缓存读到旧值数据库已更新更新数据库后未删除缓存或延迟双删的第二次删除失败检查删除任务日志查看是否有删除失败重试缓存里有数据但命中率仍然低key分布不均匀热点key被拆分到不同节点检查key分布统计各节点内存占用缓存雪崩数据库压力突变大量key设置了相同的过期时间查看缓存key的ttl分布检查是否有批量写入同一key并发打到数据库互斥锁未生效或锁粒度过大检查锁的timeout参数确认锁释放逻辑跨语言服务读到乱码序列化协议不统一检查value的前几个字节判断序列化格式缓存内存暴涨key未设置过期时间或过期时间过长用redis-cli --bigkeys扫描大key这张表我打印出来贴在了工位上每次排查缓存问题先对照一遍能省不少时间。5.2 一次缓存雪崩的完整排查过程有一次大促活动刚开始监控告警就响了Redis的命中率在10分钟内从98%掉到60%数据库CPU从20%飙到80%。我的排查过程是这样的第一步先看Redis层面的指标。用redis-cli INFO stats查看keyspace_hits和keyspace_misses确认命中率下降是全局性的还是某个db的。同时用redis-cli --bigkeys扫描有没有大key导致阻塞。第二步看慢查询日志。SLOWLOG GET 50输出最近50条慢命令发现大量DEL命令耗时超过200ms。这说明有代码在循环删除大量key而不是单个key的删除。第三步看应用日志。发现一个定时任务在大促开始时清除了所有“活动页缓存”的key因为运营活动修改了页面配置代码里简单粗暴地删除了整个前缀的所有key。这个操作让Redis里几千个活动相关的key同时失效引发了短暂的缓存雪崩。定位到问题后修复方案是取消前缀删除逻辑改为按具体key精确更新同时给“活动页缓存”设置随机过期时间避免后续再出现集体过期。整个过程从发现到完成修复大概花了40分钟但因为数据库扛住了压力业务没有发生中断。这个案例让我养成了一个习惯任何批量删除缓存的操作上线前都要经过代码评审并且要有灰度开关防止类似的事故再次发生。5.3 缓存监控与自愈关键的指标和告警缓存体系上线之后监控是保证长期稳定运行的关键。我重点盯五个指标命中率是最直观的缓存健康指标。正常情况下应该稳定在90%以上如果出现持续下降说明缓存策略需要调整。命中率波动大可能是缓存过期时间设置不合理或者是数据访问模式发生了变化。内存使用率反映缓存的容量水位。Redis会在内存达到maxmemory时触发逐出策略如果设置的逐出策略是allkeys-lru那一些重要的key可能会被淘汰引发缓存穿透。我一般给Redis设置maxmemory-policy volatile-lru只逐出设置了过期时间的key同时给重要的业务key设置noeviction级别的保护。慢查询次数直接反映缓存操作的效率。Redis默认慢查询阈值是100ms这样太保守了我会改成10ms把慢命令暴露得更充分。命令执行超过10ms经常是大key操作或者使用KEYS命令导致的需要及时优化。连接数变化可以反映客户端是否存在连接泄漏。Redis的最大连接数默认是10000实际生产中常会因为连接池配置不当导致连接数打满。我见过一个Java服务因为Lettuce连接池的maxTotal设置过小而频繁重建连接反而加剧了Redis的负载。主从同步延迟是分布式缓存特有的监控项。Redis Cluster里从节点同步延迟如果超过几秒意味着主从切换时有数据丢失的风险。我配置了告警当同步延迟超过2秒时触发通知需要人工判断是否需要进入只读降级模式。监控之外我还做了一层自愈机制当缓存命中率低于阈值时自动触发缓存预热任务把热点数据提前加载到缓存中而不是等用户请求触发缓存重建。这个预热任务在白名单范围内执行不会对数据库造成压力。实际跑下来这层自愈把缓存恢复正常的时间从分钟级缩短到了秒级。6. 一些实践心得这套缓存体系从单机缓存一路演进到分布式一致性方案前前后后经历了大半年时间。我最大的一个体会是缓存一致性的实现方案没有银弹关键是先梳理清楚业务对一致性的容忍度。对于允许最终一致的场景比如用户昵称、商品详情Cache Aside加延迟双删就够用了不必追求复杂的强一致方案。对于要求强一致的场景比如库存、余额与其在缓存层硬做一致性不如把核心状态放到数据库让缓存只承担读加速的职责再通过事务消息保证最终一致。另外一个感触是多语言团队里最难的不是某个语言写不出缓存代码而是不同语言的开发者对“缓存怎么做才是对的”没有统一认知。我在团队里推动了几项标准化动作统一缓存key的命名规范统一序列化协议统一缓存客户端的拦截器埋点。这些规范落地后跨语言的缓存问题减少了至少一半。最后给还在为缓存问题挠头的朋友一个建议不要一上来就追求复杂的分布式缓存架构先把单机缓存的失效场景逐个想明白把基础的防护手段用好再根据业务体量决定是否走向分布式。缓存体系是一点点长出来的不是设计出来的。我们走过的弯路、踩过的坑都是这个体系的一部分。
返回列表