ARTICLE DETAIL

资讯详情

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

多级缓存一致性实战:本地缓存与分布式缓存如何协同

多级缓存一致性实战:本地缓存与分布式缓存如何协同 2022年我面美团面试官问过这道题当时我把标准答案背得滚瓜烂熟——分布式缓存支持集群共享、容量能横向扩展、本地缓存是JVM堆内读写、每个节点各存一份、数据不一致……自认为答得滴水不漏。结果面试官紧跟了一句“那多级缓存一致性你怎么保证”我一下就卡住了因为那时候我确实没在生产环境里亲手处理过多级缓存的一致性。后来我在一家电商公司做订单服务的缓存改造真真切切因为本地缓存和Redis并用搞出一场数据不一致事故才彻底想明白这道题背后的工程逻辑。这篇文章不打算给你念标准答案而是从一个真实系统的视角把“为什么要用分布式缓存”“本地缓存的边界在哪里”“多级缓存一致性到底怎么做”这三件事讲透。适合准备大厂后端面试的同学也适合正在设计缓存架构、或者被缓存一致性坑过的同行。1. 面试官抛出这道题背后到底在考察什么这道题在面经里出现频率很高但绝大多数人答得都太浅。先把这个“题眼”挖开后面所有的技术讨论才有意义。1.1 递进式追问从“知道”到“做过”的距离“为什么要用分布式缓存本地缓存呢”这是一个标准的递进式追问。面试官想看的不是你背没背过Redis和Caffeine的对比而是你能不能回答这三个层次的问题第一层架构层面。你的应用是什么部署形态单机还是多实例有没有多机房如果服务只部署一台机器本地缓存完全够用根本不需要引入Redis。面试官问“为什么用分布式缓存”本质是在问“你的系统是不是已经大到单机内存装不下、单实例扛不住流量了”。第二层性能层面。你对“快”的理解是不是有数量级的概念。本地缓存是纳秒到微秒级别分布式缓存加一次网络往返通常是零点几毫秒到几毫秒数据库查询是几毫秒到几十毫秒。每一层差一个数量级这才是多级缓存存在的根本原因。第三层一致性层面。这也是最核心的。数据变更后你靠什么机制让所有节点的缓存统一这一层如果答不清楚前面说得再流畅也会被打回原形。1.2 一次真实事故多级缓存如何把订单状态搞脏我在那家电商公司接手订单服务的时候系统已经在线上跑了大半年。订单查询接口最热高峰期QPS能到两万多Redis的读写压力很大平均响应时间一度到了50ms。前一个团队的做法很粗暴在服务本地加了一层Caffeine缓存把订单状态、买家昵称这些热点数据放进去过期时间设了5分钟。看起来挺合理——绝大多数读请求在本地就返回了Redis压力骤降接口响应时间降到5ms以内。问题出在一次售后状态变更上。用户在后台申请退款订单服务更新了数据库里的订单状态是“退款中”然后删除了Redis里的缓存但本地Caffeine缓存还在并且保存的是旧状态“待付款”。用户刷新订单详情页请求落在不同的服务实例上有的实例返回新状态有的实例返回旧状态。客服那边连续收到好几个用户反馈说“我已经申请退款了怎么订单还显示待付款”。排查下来就是本地缓存和Redis数据不一致导致的。修改只清掉了Redis那一层本地缓存完全不知道数据已经变了。这个事故让我意识到多级缓存真正的复杂度根本不在“读多快”而在“数据变了之后怎么让每一层缓存都感知到”。后面我把这个问题整个重新做了一遍文章后半部分会详细拆解。2. 本地缓存性能拉满但集群环境藏着一颗暗雷本地缓存是企业级应用里被低估的一种技术。很多人一听到“分布式缓存”就两眼放光觉得Redis是万能的往往忽略了本地缓存的价值。2.1 三种常见的本地缓存实现本地缓存的实现方式大致有三个发展阶段也能看出一个团队对性能的追求程度。第一种直接用ConcurrentHashMap自己维护。这是最原始的方式什么逻辑都要自己写过期时间怎么判断、容量满了谁先被淘汰、并发读写怎么保证线程安全。写起来最快但最容易出隐蔽Bug。第二种用Guava Cache。它提供了过期策略、容量上限、LRU淘汰、刷新机制API也比较友好是很多老项目的标配。第三种Caffeine。它基于Java 8优化内部实现了W-TinyLFU淘汰算法读写命中率比Guava Cache的LRU要好性能在本地缓存里基本是天花板。我现在的项目里本地缓存层全部用Caffeine。// Caffeine 本地缓存基础用法 CacheString, OrderDetail localCache Caffeine.newBuilder() .maximumSize(10_000) // 最多缓存1万个key .expireAfterWrite(30, TimeUnit.SECONDS) // 写后30秒过期 .recordStats() // 开启命中率统计 .build(); OrderDetail detail localCache.get(orderId, key - // 回源逻辑先查Redis再查数据库 loadFromRedisOrDB(key) );使用上很简单真正复杂的是要不要用、怎么用。2.2 本地缓存为什么能快到“不合理”本地缓存的读性能是一个让很多人没有直观数字的概念。我在压测环境里测过Caffeine的读操作延迟大约在20到50纳秒也就是0.02到0.05微秒。而一次Redis请求即使内网延迟只有0.2ms也是本地缓存的几千倍。这个差距来自哪里本地缓存就是一个纯粹的内存对象查找没有网络IO没有序列化没有socket读写CPU直接通过指针访问堆内存。用生活里的例子类比本地缓存就好比你口袋里放了一本常用通讯录翻一下眼睛就能看到电话号码分布式缓存相当于你打电话给另一个城市的查号台让对方告诉你号码。虽然查号台也很专业、信息更全但多了一次电话沟通速度就慢下来了。这个数量级的性能差异就是多级缓存架构存在的第一性原理尽量让请求在离CPU最近的地方命中实在命中不了才逐级向下穿透。2.3 本地缓存在集群环境下的三个致命问题本地缓存最大的问题是“每台机器各存一份”这引出了三个连锁问题。第一个数据孤岛。应用部署了10个实例同一个key在每台JVM里都可能有一份缓存而且过期时间、更新时机都不一致。用户请求负载均衡到不同实例拿到的数据版本可能完全不一样。第二个内存瓶颈。JVM堆内存是有限的本地缓存占用的空间直接挤压业务对象的内存。如果每个实例缓存10万个订单详情每个对象平均1KB就是100MB堆内存在多实例部署下这个空间是被乘了N份的整体浪费非常严重。第三个一致性黑洞。这是最头痛的。Redis里的缓存可以通过删除key来同步但本地缓存分散在各实例内部没有任何中央节点能直接删除它们。除非自己实现一套失效广播机制否则本地缓存就会一直保存旧值直到自然过期。2.4 什么场景可以放心用本地缓存也不是说本地缓存就不能用关键看数据场景。我总结了三类适合放本地缓存的数据配置项、字典表这类低频变更数据变更频率可能是小时级甚至天级。允许短时间不一致的聚合统计结果比如首页的商品推荐列表。热点极高且单个key体积小的数据比如秒杀商品的库存状态但要注意配合强制失效通道。在这些场景里本地缓存的性能红利远大于一致性风险。真正危险的是把订单状态、账户余额这类强一致数据放进本地缓存还不做失效通知。3. 分布式缓存解决共享问题的关键跳板如果本地缓存已经足够快为什么还要引入分布式缓存带着前面的问题看这一章会很容易理解。3.1 分布式缓存的核心价值从“每台机器各存一份”到“全局共享一份”分布式缓存把数据从“应用本地”挪到了“独立的缓存集群”所有应用实例访问的是同一份数据。最典型的实现就是Redis Cluster。这样带来的改变是本质性的不管请求落在哪个应用实例上拿到的都是同一份缓存数据的一致性边界从“单机”扩大到了“集群”。仍以通讯录类比。本地缓存是每个人口袋里都装一份可能过期的通讯录分布式缓存是公司统一维护的一个查号台虽然查号要打一个电话但所有人查到的最新号码只有一个来源。从架构上看分布式缓存还扛起了几件本地缓存做不到的事容量横向扩展内存不够了加节点就行多个服务之间共享热点数据比如订单服务写入的缓存它可以被支付服务、物流服务读取数据持久化能力Redis的AOF和RDB机制提供了基本的数据恢复能力。3.2 分布式缓存付出的代价网络、序列化与集群运维引入分布式缓存不是免费的代价非常现实。最明显的是网络开销。每次缓存读写都要经过一次TCP或长连接通信即使内网延迟很低也远不如JVM堆内访问。其次是序列化和反序列化。对象从应用写入Redis时要序列化成字节数组取出时要反序列化回对象。如果缓存的是一个嵌套很深的订单对象序列化成本可能比网络IO还高。还有一个容易被忽视的代价是运维复杂度。单机Redis好维护一旦上了Cluster模式要考虑分片策略、主从复制、哨兵切换、扩容迁移、内存碎片整理。这些对团队的基础设施能力提出了明确要求不是“引入一个依赖”就能了事的。3.3 三层数据访问方式的核心对比我用一个表格把这三种数据访问方式摆在一起看性能维度的差异一目了然。访问方式访问延迟量级共享性网络开销典型场景本地缓存Caffeine纳秒级0.02~0.05μs单机独享无配置字典、热点数据分布式缓存Redis亚毫秒到毫秒级全局共享一次网络往返业务数据缓存、分布式锁数据库MySQL毫秒级含磁盘IO全局共享网络存储IO持久化存储、强一致性数据强调了三点第一每一层访问方式高出两个以上数量级第二访问速度越快数据一致性维护成本越高第三没有一种存储能同时满足高性能、强一致和低成本。3.4 分布式缓存也扛不住的三类冲击分布式缓存解决了“共享”问题但缓存系统本身的问题它一个都没解决。缓存穿透、缓存击穿、缓存雪崩依然存在而且在多级缓存架构下这些问题的破坏力可能被放大。缓存穿透查询一个必然不存在的keyRedis里没有每次都要穿透到数据库。缓存击穿某个热点key在Redis中过期的一瞬间大量请求同时回源数据库。缓存雪崩大量key在同一时间过期或者Redis集群整体不可用数据库直接被打垮。这也是多级缓存的重要价值之一当Redis发生故障或大量key过期时本地缓存还能临时顶上为用户提供降级但可用的服务。4. 多级缓存架构让各层缓存各司其职多级缓存不是把本地缓存和Redis简单叠在一起就完了而是要设计每一层的职责边界让整个读链路像一个分工明确的流水线而不是一团乱麻。4.1 一个读请求在三级缓存中的完整旅程我最终落地的架构是三层本地缓存Caffeine在最前面Redis在中间数据库在最底层。一个标准的读请求会经历以下步骤请求进入应用先查本地Caffeine缓存。命中就直接返回整个流程结束耗时通常不到1ms。本地缓存未命中查Redis。Redis命中就回填本地缓存并返回耗时通常在1ms到3ms。Redis也未命中回源数据库查询耗时通常在5ms到20ms。拿到数据后先写Redis再写本地缓存最后返回给调用方。这套流程的逻辑是用内存中最快的本地缓存挡住大部分流量用Redis挡住次一级的流量数据库只承担真正无法命中缓存的读请求。4.2 为什么是两级而不是更多级有人会问既然多一级缓存多一点性能那加到五级六级行不行答案是不行因为每增加一级缓存都引入一个一致性问题源和运维复杂度。两级缓存的黄金分割点在于本地缓存已经是最快的访问方式继续增加接近CPU的存储介质性能提升微乎其微Redis已经覆盖了全局共享的诉求继续增加中间层只会让数据同步链路更长。经验的做法是先把两级缓存用好比盲目做更多级更落地。4.3 数据分治什么样的数据进哪一层不是所有数据都需要同时进两级缓存。我按数据特征把缓存数据类型做了划分强一致数据比如订单状态、支付流水状态不进本地缓存只进Redis并且更新数据库后立刻删缓存。本地缓存只保留秒级短过期时间或干脆不缓存。弱一致数据比如商品详情、店铺信息、用户昵称本地缓存加Redis双写本地缓存过期时间设为30秒到5分钟。静态数据比如配置字典本地缓存失效时间拉长到10分钟以上Redis作为兜底防止本地缓存长时间未更新的极端情况。这个划分原则是数据越重要允许缓存存在的层级就越少过期时间就越短。4.4 热点数据识别与本地缓存准入控制多级缓存有一个隐患就是本地缓存把所有流量都扛住了Redis的命中率直线下降。由于本地缓存存的是大而全的数据内存压力会很大且大部分都命中不了几次白占空间。我后来加了热点数据识别机制。通过Redis的HotKey探测或者本地统计把请求量排名靠前的key筛选出来只允许这些热点key进入本地缓存普通key直接从Redis读。实现思路并不复杂// 伪代码本地缓存准入控制 public OrderDetail getOrderDetail(String orderId) { // 1. 先判断是否热点key不是热点直接走Redis if (!hotKeyDetector.isHotKey(orderId)) { return getFromRedis(orderId); } // 2. 热点key才走本地缓存 OrderDetail detail localCache.getIfPresent(orderId); if (detail null) { detail getFromRedis(orderId); localCache.put(orderId, detail); } return detail; }这样做的好处是本地缓存只服务真正高频的key内存利用率高一致性问题影响面也小。一个订单如果不那么热门它变了就变了本地缓存里没有它永远不会产生不一致。5. 多级缓存一致性从根因到可落地的四条路径多级缓存是整个系统性能的放大器但一致性是唯一的命门。回到那场事故问题的本质是数据变更后新数据没有及时覆盖所有缓存层。这一章的核心就是讲清楚不一致怎么产生的以及我踩过坑后梳理出的几条保底路径。5.1 不一致的三个来源我把多级缓存不一致归纳为三个来源第一更新顺序问题。数据库更新、Redis删除、本地缓存失效这三个动作的先后顺序一旦不对就会出现“旧值覆盖新值”的情况。比如先删除缓存再更新数据库更新数据库的间隙有请求来查会把旧值重新写进缓存。第二并发读写竞争。在同一时刻有线程写数据、有线程读数据。读线程在写线程更新数据库之前把旧值填入缓存写线程之后虽然删了缓存但可能删除动作发生在读线程回填之后导致新删除的key被旧值重新占位。第三本地缓存与分布式缓存之间的通知缺失。这是最容易踩坑的。修改了数据库、删除了Redis但如果不知道如何通知各实例的本地缓存那本地缓存只能等到自然过期过期前的这段时间就是不一致窗口。5.2 Cache Aside模式为什么“先更新DB再删缓存”是主流绝大多数缓存系统的写路径都采用Cache Aside模式核心原则概括成一句话读的时候先读缓存读不到读数据库然后回填缓存写的时候先更新数据库然后删除缓存。为什么是“删除缓存”而不是“更新缓存”这是很多初学者最容易纠结的地方。删除比更新更安全。因为更新操作需要知道缓存里的数据结构如果缓存里根本没有这个key更新就是白费力气而且并发场景下两个线程同时更新缓存后更新的可能覆盖先更新的值而这个值可能来自一个过期的数据库读取。删除就很简单直接让缓存失效等下次读取时回源数据库拿到的必然是最新值。“先更新DB再删缓存”的兼容性也很好。即使删除缓存失败导致一段时间读旧值缓存过期时间兜底后最终一致。但如果说先删缓存再更新DB那么更新DB期间的读请求都会击穿到数据库并且在旧值和新值之间产生一个错误的缓存回填窗口处理难度更大。5.3 延迟双删补偿掉并发写读的时间窗口只执行“更新DB、删除Redis”在并发读写激烈的时候还是会出现一个问题。举个例子线程A更新数据库成新值N然后删除了Redis线程B在A删除Redis之前读到了数据库的旧值O正要把O回填到Redis如果B的回填发生在A删除之后那么Redis里留存的就是旧值O之后所有读取都会命中这个旧值直到下次缓存过期。解决思路是延迟双删。在第一次删除缓存后等待一段时间再删一次。这个等待时间要保证所有在途的读请求都已经完成缓存回填这样第二次删除就能把那个错误的旧值清掉。// 延迟双删的简单实现 public void updateOrderStatus(String orderId, int newStatus) { // 第1步更新数据库 orderDao.updateStatus(orderId, newStatus); // 第2步删除Redis缓存 redisTemplate.delete(order: orderId); // 第3步本地缓存主动失效关键 localCache.invalidate(order: orderId); // 第4步延迟一段时间再次删除Redis缓存 executor.schedule(() - { redisTemplate.delete(order: orderId); }, 500, TimeUnit.MILLISECONDS); }这个500ms的延迟时间不是拍脑袋定的要根据实际的“读请求回填耗时”估算。正常情况下一个读请求从数据库拿到数据再回填Redis几十毫秒就够了500ms已经是一个比较稳妥的上限。但对耗时很长的慢查询场景这个值要调大。延迟双删的问题是如果第二次删除失败旧值还是有可能残留而且多级缓存情况下本地缓存的删除也需要纳入这个流程单纯只删Redis是解决不了本地缓存不一致的。所以延迟双删不应该是最终方案只能算一个辅助手段。5.4 基于版本号保证本地缓存与Redis的一致性本地缓存与Redis一致性的核心矛盾是本地缓存没有中心化失效手段。我的做法是给每个缓存key配一个版本号版本号存放在Redis里本地缓存存储的时候同时记住版本号。每次读请求到本地缓存时会拿记录的版本号和Redis里的最新版本号比较不一致就重新加载。// 版本号方案本地缓存带上版本号 public OrderDetail getOrderDetail(String orderId) { // 1. 先从本地缓存取数据 VersionedValueOrderDetail local localCache.getIfPresent(orderId); // 2. 从Redis获取当前版本号 long remoteVersion getVersionFromRedis(orderId); if (local ! null local.getVersion() remoteVersion) { return local.getValue(); // 版本号匹配本地缓存可用 } // 3. 版本号不一致或本地无缓存回源Redis或数据库 OrderDetail detail loadFromRedis(orderId); localCache.put(orderId, new VersionedValue(detail, remoteVersion)); return detail; }这个方案的核心思路是把“主动通知本地缓存失效”变成“每次读取时主动检查版本号”。每次读多一次Redis版本号查询整体开销还是远低于回源数据库。在本地缓存命中率高的前提下这个方案能大幅度缩短不一致窗口能达到最终一致。版本号可以是一个纯粹自增的Long值也可以是一个timestamp。生产环境里我用过Redis的Hash结构存储版本号key是业务IDfield是版本号每次数据更新时对field执行INCR操作。这样删除缓存或者重置版本的成本都很低。5.5 binlog订阅用数据库增量日志驱动缓存失效如果追求更彻底的一致性可以考虑使用binlog订阅方案。通过监听MySQL的binlog变更将数据变更事件发送到消息队列再由消费端完成Redis缓存删除和本地缓存失效广播。不画图了用文字描述整体链路应用更新数据库MySQL把变更记录写进binlogCanal伪装成MySQL从库拉取binlog并解析出结构化变更事件发送到MQ处理服务消费事件后执行Redis删除同时通过Redis Pub/Sub或RPC广播通知所有应用实例清理本地缓存。这个方案的优势是业务代码里不需要手动写“删缓存”的逻辑缓存失效动作完全由binlog驱动天然规避了“忘记删缓存”“删了没删干净”等人为失误。代价是引入了一套新的基础设施组件Canal和MQ运维和排查链路更长。5.6 一致性方案如何选型没有银弹只有适度平衡我把上面提到的多个路径按“一致性强度”和“落地成本”做了个对比方便你结合自己的场景去选型。方案一致性窗口落地成本适用场景只删Redis缓存秒级到分钟级低一致性要求不高的读多写少场景延迟双删 过期兜底毫秒级到秒级低一般业务缓存推荐配合版本号使用版本号校验毫秒级中本地缓存场景的最佳伴侣binlog订阅毫秒级高核心链路数据变更频繁且要求高一致没有哪个方案是万能的关键是找到“不一致窗口可接受”与“基础设施复杂度可接受”之间的平衡点。我的经验是业务越核心越要把方案往底下那两行走。6. 一套经过线上验证的落地方案与三个最值得注意的坑最后分享我现在线上运行的完整方案它不追求理论上的绝对一致但已经被真实大流量反复验证过足够可靠也好维护。6.1 我最终采用的方案组合我现在在订单服务里的缓存方案是这么组合的数据库更新走Cache Aside模式先更新数据库再删除Redis缓存。Redis删除之后通过Redis Pub/Sub发布一条“缓存失效”消息各应用实例订阅这条消息收到后清理本地Caffeine缓存。对订单状态这类高一致数据不缓存到本地只走Redis一层。对商品详情这类弱一致数据本地缓存设30秒短过期Redis设10分钟过期自然过期兜底。对希望进一步降低不一致窗口的本地缓存key启用版本号校验。这套组合看起来不像某个单一方案的简化但它能保证每个不一致窗口都被至少两类机制覆盖。6.2 本地缓存失效广播为什么我选择Redis Pub/Sub本地缓存失效广播有两种常见实现Redis Pub/Sub和RPC逐个通知。我最终选了Redis Pub/Sub。原因有三点第一Redis是缓存层分布式组件的中心应用实例都连接同一个Redis集群Pub/Sub天然就是一个广播总线不需要额外维护一套RPC连接关系第二Pub/Sub的消息是即时的不像版本号轮询还要等下一次读取才校验第三写一个简单、高效。代码实现也不复杂// 发布端更新DB并删除缓存后广播本地缓存失效 public void updateOrder(Order order) { orderDao.update(order); redisTemplate.delete(order: order.getId()); // 广播本地缓存失效 redisTemplate.convertAndSend(cache.invalidate, order.getId()); } // 订阅端在应用启动时订阅收到消息清理本地缓存 Component public class CacheInvalidateSubscriber { Resource private CacheString, OrderDetail localCache; PostConstruct public void subscribe() { ConnectionFactory factory redisTemplate.getConnectionFactory(); RedisConnection connection factory.getConnection(); connection.subscribe((message, pattern) - { String orderId new String(message.getBody()); localCache.invalidate(orderId); }, cache.invalid.getBytes()); } }注意一个细节Pub/Sub是即发即弃的消息发送时如果某个应用实例恰好断线或者消费线程阻塞那条失效消息就丢了本地缓存会一直留到自然过期。所以我同时把本地缓存的过期时间控制在30秒以内让“消息丢失”的最坏情况等同于“临时性短时间读到旧值”不会无限期不一致。6.3 三个最值得注意的坑这几个坑都是我在线上踩过的特别拿出来说。第一个坑本地缓存回填覆盖新值。在更新数据库后删除Redis、广播失效的过程中如果有个读请求刚好多级缓存miss它可能从数据库读到旧值然后回填本地缓存和Redis把本该失效的旧值又写回去了。这个坑的防护主要靠版本号校验。每次回填本地缓存时记录版本号读取时和Redis版本号比对不一致就重新加载。第二个坑Pub/Sub消息丢失。Redis Pub/Sub没有持久化一旦发布消息时消费者没准备好消息就找不回来了。我用来兜底的办法除了刚才说的短过期时间以外还会在数据变更后对外提供一个刷新接口运营系统在批量修改后主动调用强制刷新缓存。第三个坑Redis集群删除缓存没有“事务”保护。更新数据库成功之后、删除Redis之前应用进程崩溃了缓存里还是旧值。这个问题的粗粒度方案是给缓存设置一个业务可接受的过期时间保证即使删除失败最坏情况也只是过期后重新从DB加载。没有分布式事务能做到物理上的完全原子能做的就是缩短不一致窗口并让窗口内的旧值在业务上无害。6.4 怎么监控缓存一致性问题一致性问题发生的时候往往没有报错只会表现为“数据偶尔对不上”所以必须有监控手段。我现在的做法是三个指标第一本地缓存和Redis的命中率如果本地缓存命中率暴跌很可能说明广播失效消息频繁触发可能是数据变更太频繁第二缓存删除失败的告警每次delete操作如果返回失败或者Pub/Sub广播发送异常要立刻告警第三版本号不一致率统计定期抽样比对本地缓存版本号和Redis版本号如果发现大量不一致就要检查是不是广播链路出了问题。有一个更直接的“黑盒监控”方法在测试环境跑一个定时任务每隔几秒随机选择一个订单强制走“更新数据库删除缓存”的完整链路随后以调用方身份反复读取这个订单检查是否能持续读到新值。一旦发现读到旧值说明某个缓存层级没有按预期失效定位起来非常方便。后来美团那个面试官又问我“如果让你设计一套多级缓存你会怎么保证一致性”我把Pub/Sub广播加上短过期时间、热点key准入、版本号校验这套思路讲了一遍他没再追问细节。因为这套答案是从真实故障里总结出来的每一句话背后都有代价和取舍这不是标准答案能替代的。最后分享一个我自己的体会缓存一致性这个问题目标是让用户感知不到不一致而不是实现理论上的绝对一致。只要不一致窗口小于业务可容忍范围并且最坏情况下不会带来资金或安全风险就是一套合格的缓存架构。想清楚这一点很多方案选型就变得清楚多了。
返回列表