拓冰建站拓冰建站
首页 / 资讯中心 / 正文

缓存更新策略全解析:从一致性保障到高并发实战选型

我自己在高性能网站设计上踩过最大的坑就是缓存更新。缓存命中率上去了接口响应时间下来了结果一更新数据用户刷出来的全是旧值或者一会儿新一会儿旧后台还查不到原因。后来把缓存更新的各种套路捋清楚才算把这块彻底稳住。这篇内容围绕缓存更新这个核心话题整理了我在实际项目中用过的策略、踩过的坑、以及可以照抄的选型方案和排障方法适合正在做高并发接口、读写分离架构、以及被缓存一致性折磨的后端开发同学参考。如果你刚接触缓存也能从里面拿到一套可以直接落地的思路。1. 缓存更新到底在解决什么问题1.1 高性能网站为什么绕不开缓存更新任何一个读多写少的系统都会把缓存放在数据库前面。缓存能把读请求的响应时间从几十毫秒压到几毫秒这是高性能网站设计里最基础的一招。但缓存引入之后最头疼的问题就是数据库里的数据变了缓存里的旧数据怎么办。直接不更新用户会一直读到旧值严重的话会出现库存超卖、订单状态错乱每次写操作都同步更新缓存写路径又会被拖慢高并发下还容易产生缓存和数据库之间的一致性问题。所以缓存更新不是简单地在写入后刷一下缓存而是一套需要根据业务场景精细设计的策略。我自己做电商后端时遇到过最典型的情况商品价格改了管理后台改完数据库用户端还是显示旧价格客服那边接到大量投诉。原因就是当时只做了缓存过期TTL还设得很长数据库更新后缓存要等几分钟才失效。这个案例让我意识到缓存更新策略必须在系统设计阶段就定好而不是上线后出了问题再补。1.2 缓存不一致的几种典型现场先把问题定义清楚。缓存和数据库不一致常见有下面几种现场数据库更新成功缓存更新失败导致读取到旧数据并发请求下一个线程写数据库另一个线程写缓存写入顺序错乱删缓存和更新数据库之间没有原子性中间态被其他请求读到缓存过期时间设置过长数据库已经变更但缓存迟迟不刷新每一种现场背后对应的解决思路都不一样。比如缓存更新失败的场景核心是补偿重试机制并发写顺序错乱的场景核心是串行化或者版本控制。把这些现场记住后面选策略的时候就能对号入座。2. 主流缓存更新策略逐一拆解2.1 Cache Aside最常用但最容易踩坑Cache Aside 是大部分团队的首选方案思路很简单读的时候先读缓存读不到就读数据库然后把数据写回缓存写的时候先更新数据库然后删除缓存。这个方案的精髓在于“更新缓存”这个动作换成了“删除缓存”。删掉之后下一次读请求会触发缓存重建读到的一定是新数据。我最早不理解为什么更新数据库后不是直接改缓存而是删掉重写后来在高并发场景下才明白直接改缓存存在竞争条件两个并发写请求同时更新数据库和缓存可能会出现数据库里是B值、缓存里却是A值的错乱。删除缓存则规避了这个风险让缓存重建发生在读路径上配合单飞的并发控制一致性反而更好。但Cache Aside也有个经典问题先更新数据库还是先删缓存。如果先删缓存再更新数据库那么在删缓存和更新数据库之间的时间窗口里有一个读请求发现缓存为空就会去读数据库把旧数据写回缓存。等数据库更新完成后这个旧缓存值会一直存活到过期。所以更稳妥的顺序是先更新数据库再删除缓存。这样至少能保证在数据库更新完成之前缓存里的值还是有效的顶多是读到旧值而不会出现新数据库值搭配旧缓存值的永久不一致。Cache Aside适合大多数业务场景尤其是读多写少、对一致性要求不是极端严格的场景。实现简单、容易理解出问题了也容易排查。2.2 Read Through / Write Through把更新交给缓存组件Read Through 和 Write Through 在架构上的特点是应用程序不直接操作缓存和数据库而是把读写请求都交给缓存组件由缓存组件负责和数据库同步。以 Write Through 为例写请求先写缓存缓存组件同步把数据写入数据库两个操作作为一个整体对外暴露。这样做的好处是应用层不需要关心先写哪个、再删哪个一致性逻辑收拢到了缓存组件内部。Read Through 则是读请求直接走缓存缓存里没有数据时由缓存组件自己加载数据库数据并回填。这和 Cache Aside 的“应用层手动回填”不同应用层只知道缓存里有数据不用管这个数据是怎么来的。我实际使用下来这个方案适合那些有成熟缓存中间件支持的团队比如用 Redis 自定义加载器或者使用带持久化能力的缓存系统。但要注意Write Through 会拉长写路径的延迟因为每次写操作都要同步写数据库。如果写入量很大需要评估是不是能接受这个性能损失。2.3 Write Behind用异步换吞吐Write Behind 也叫 Write Back它的核心思路是写请求只更新缓存立即返回成功缓存组件异步地把数据批量写入数据库。这个策略的最大优点是写吞吐极高因为写操作不再同步等待数据库响应。但我必须泼一盆冷水异步写意味着数据存在丢失窗口。如果缓存服务在异步刷盘之前宕机这批数据就丢了。所以在金融、订单这类强一致场景我不会推荐 Write Behind它更适合计数器、浏览量、点赞数这类允许轻微丢失的数据。使用 Write Behind 还需要注意写入顺序问题。比如同一个用户的多个操作异步线程如果乱序落库会导致最终数据和用户操作顺序不一致。解决办法是使用队列按 key 维度把操作串行化保证同一个 key 的写操作按照到达顺序执行。在我维护过的点赞系统里就是用 Write Behind 把每日千万级的点赞写入压到了很低延迟异步批量落库之后数据库负载也非常平稳。但为了防丢数据我额外做了定时全量对账一旦发现缓存和数据库的差值超过阈值就触发重放修复。3. 策略选型背后的计算与决策3.1 更新顺序决定一致性边界无论是 Cache Aside 还是其他变种最关键的一个决策点是“数据库更新”和“缓存变更”的先后顺序。我梳理过一张决策表可以直接对照使用顺序一致性表现适用场景先更新数据库再删缓存数据库更新期间短暂旧读更新完成后下次读即新值一致性较好绝大多数业务先删缓存再更新数据库删缓存后到数据库更新完成前读请求会把旧值回填可能出现旧值长期存活对短暂不一致敏感的读多写少场景需配合延迟双删先更新数据库再更新缓存并发更新数据库时易出现缓存与数据库值错位不推荐几乎不适用先更新缓存再更新数据库缓存与数据库长时间不一致且缓存可能被回源覆盖强烈不推荐我自己实测下来最稳的组合是“先更新数据库再删除缓存”。之前在订单状态流转场景中把原先的“先删缓存再更新DB”改成这个顺序之后用户端看到状态回退的概率直接降到了零。3.2 延迟双删的适用边界与参数选择在一些极端并发场景下单纯“先更新数据库再删除缓存”还是不够。比如两个并发读请求同时发现缓存为空同时回源数据库其中一个回源拿到的是旧值并写回缓存可能导致旧数据覆盖新数据。针对这种“缓存重建期间的旧值回填”业界常用的手段是延迟双删先删一次缓存更新数据库然后隔一段时间再删一次缓存。第二次删除是为了清掉第一次删除到数据库更新完成之间可能被回填的旧值。延迟时间的选择是个关键参数。我一般按“缓存重建耗时”的倍数来估算。假设一次数据库查询耗时 10ms缓存回填耗时 2ms那么第二次删除可以放在 50ms 到 200ms 之间。这个时间要大于“某个读请求从发现缓存为空到完成回填”的最长可能时间。设得太短旧值可能还没写完就被更新事件覆盖了设得太长会无谓增加缓存空窗期。延迟双删解决的是小概率的并发覆盖问题不是银弹。它引入了额外的一次删除操作和定时任务增加了系统复杂度。如果业务对一致性的要求没那么高普通双删甚至单删已经够用。3.3 不同业务场景的选型对照根据业务特性选缓存更新策略比单纯追求某种“最佳方案”重要得多。我按自己接触过的典型业务整理了下面的对照表业务场景推荐策略核心理由商品详情页Cache Aside先更新DB再删缓存读多写少删除后重建成本低库存扣减Cache Aside 延迟双删 DB乐观锁强一致防超卖缓存重建要串行化用户会话信息Cache AsideTTL较短数据可用性高短暂不一致可接受热门榜单Write Behind 定时全量重建写频繁读量巨大允许秒级延迟计数器/点赞数Write Behind 队列串行高吞吐允许少量丢失需要对账AI模型嵌入向量缓存Cache Aside 版本号模型升级后缓存必须整体失效不能等TTL每次做选型我都会问三个问题这个数据丢失或延迟一致的成本有多高写入频率和读取频率的比例是多少团队有没有能力维护额外的补偿和一致性机制想清楚这三个问题方案基本就浮出水面了。4. 实操过程一套可以落地的缓存更新方案4.1 基础设施与工具准备在动手之前先确认你手上的基础设施。我平时会准备这几样东西Redis 集群缓存组件建议开启 AOF 持久化避免缓存重启后大面积穿透消息队列用于缓存更新失败的异步重试定时任务框架用于延迟双删、对账任务数据库 binlog 订阅组件如果团队成熟可以考虑监听 binlog 自动失效缓存我这里用一个实战项目的简化版举例商品服务。数据库是 MySQL缓存是 Redis更新入口是一个管理后台接口读入口是用户端商品详情接口。4.2 核心流程实现先看写路径。商品信息更新时我采用“先更新数据库再删除缓存失败则进入重试队列”的流程def update_product(product_id, new_data): # 第一步更新数据库 db.update(product_id, new_data) # 第二步删除缓存 cache.delete(product_key(product_id)) # 第三步如果删除失败进入重试队列 if not cache.delete_success: retry_queue.push(product_id)这里有个细节删除缓存的操作不是直接把 Redis 的 del 命令发出去就完了。我会把删除动作封装成一个可重试的任务。如果 Redis 临时抖动导致删除失败任务会进入消息队列由消费者在几秒后再次尝试。读路径实现如下def get_product(product_id): # 第一步读缓存 data cache.get(product_key(product_id)) if data: return data # 第二步缓存未命中加锁回源数据库 with cache.lock(product_key(product_id)): data db.query(product_id) cache.set(product_key(product_id), data, ttl600) return data加锁这段是关键。不加锁时热点商品缓存刚失效几百个请求同时回源数据库数据库可能直接被打挂。加了锁之后只有一个请求去数据库取数据其余请求等待锁释放后直接读缓存。这个操作对高并发下的稳定性提升非常明显。TTL 的选择我一般按“业务能容忍的最长旧数据存活时间”来定。商品信息我习惯设 600 秒因为后台修改商品后希望最长 10 分钟内用户能看到新值这个 TTL 可以兜底。4.3 场景扩展AI 推理服务的缓存版本更新最近我在处理一个文本嵌入推理服务的缓存更新问题类似于 Hugging Face 的 TEI 这类服务需要把模型输出的向量结果缓存起来避免重复计算。这类缓存更新有个特殊点模型版本升级后同一个文本生成的向量可能完全不同旧缓存必须整体失效。如果只靠 TTL 过期TTL 没到之前新旧版本的向量会混用导致检索结果混乱。我的做法是在缓存 key 里加入模型版本号。比如text_embedding:v3:你是一个测试文本当模型从 v2 升级到 v3 时业务代码切换到新的 key 前缀旧数据自然被隔离。同时我会通过消息队列主动清理 v2 前缀的批量数据而不是傻等 TTL。这种做法比“更新缓存数据”更高效也避免了缓存值和模型版本不匹配的问题。这种“版本号进入缓存 key”的思路对于算法模型迭代频繁的团队特别实用。不只是 AI 服务凡是底层依赖版本会变的缓存数据都可以照搬。5. 常见问题与排查技巧实录5.1 数据不一致的定位思路如果线上已经出现了缓存和数据库不一致先别急着清缓存按下面的思路排查确认不一致是持久的还是短暂的。短暂不一致可能是延迟双删的时间窗口内持久不一致要看更新顺序是否正确。检查更新路径有没有异常。查看删除缓存是否成功是否有失败后没有补偿的记录。确认并发情况。查看是否有多个实例同时写同一个 key存在写顺序错乱的可能。观察 TTL。如果设置了过长的 TTL数据库已更新但缓存迟迟不过期也会表现出持久不一致。我记得有一次运营反馈商品价格一直不对我查了很久发现是定时任务在更新数据库时没有走我们封装的缓存失效逻辑而是直接用 SQL 脚本改了库。缓存自然不会被删除。后来所有连数据库的账户和入口都强制走统一的数据访问层问题才彻底解决。5.2 更新失败的补偿机制缓存删除失败这个事靠人盯是盯不住的。我推荐一套机制删除失败时写本地内存表记录 key 和操作时间定时任务每隔 30 秒扫描失败记录重新执行删除重试达到一定次数还失败就触发告警人工介入这套机制成本很低但能把缓存更新的可靠性提升一个量级。还有一种更彻底的做法订阅数据库 binlog每次数据变更自动触发缓存删除。这样即使业务代码漏了删除逻辑binlog 通道也能兜底。代价是要额外维护一个消费组件对团队人力有要求。5.3 环境层面的缓存异常以 Ubuntu 系统更新缓存为例这里分享一个和“高性能网站”看似无关、但运维同学经常踩的坑Ubuntu 系统更新缓存时偶尔会报错现象是在执行软件源刷新时提示部分索引下载失败或者哈希校验不一致。处理思路其实和业务缓存差不多。先把损坏的列表缓存目录清理掉再用干净的源列表重新生成索引。我在服务器上会定期做一次软件源清理避免陈旧的缓存元数据干扰日常部署。这个经验告诉我们缓存不只是 Redis 里的数据系统层面、应用层面的缓存同样需要一套更新和失效策略。对一个线上网站来说系统源缓存出错虽然不直接影响业务接口但会阻塞安全补丁更新间接影响系统的稳定性和性能表现。我习惯把所有服务器的基础镜像和软件源版本固定下来配合定期的缓存刷新任务减少这类问题。写在最后的实操心得缓存更新不是一个可以“上线后再说”的问题。我在多个项目里验证过提前设计好更新策略比事后修数据省太多时间。常用的方案其实就是先更新数据库再删缓存配合延迟双删和重试队列能覆盖绝大多数场景。遇到了 AI 模型版本升级这类特殊场景把版本号放进缓存 key 就能快速切换不用做复杂的数据迁移。如果你现在正被缓存不一致困扰我建议你先做一件事把项目中所有写数据库的入口列出来检查它们有没有统一的缓存失效逻辑。很多“诡异”的缓存问题根源就是某个旁路接口绕过缓存更新直接改了数据库。再补一个小技巧删除缓存时不要用 select-then-delete 这种组合直接 del 就好。某些缓存组件看起来支持“先比较再删除”的操作在高并发下反而会引入新的竞态条件简单直接往往最可靠。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门