怎样实现同步数据库与缓存数据

wen 实用脚本 29

本文目录导读:

怎样实现同步数据库与缓存数据

  1. 核心原则:先更新数据库,再操作缓存
  2. 策略一:Cache-Aside Pattern(旁路缓存模式)—— 最常用
  3. 策略二:Read/Write Through Pattern(读写穿透模式)
  4. 策略三:Write Behind Cache Pattern(异步回写模式)
  5. 策略四:基于 Binlog/CDC(变更数据捕获)的最终一致性方案
  6. 各策略对比总结
  7. 两种“坑”与最佳实践
  8. 推荐方案(针对不同阶段)

实现数据库与缓存数据同步是保证系统数据一致性的关键,尤其在微服务和高并发场景下,没有一种“万能”方案,需要根据业务对一致性、性能和可用性的容忍度来选择。

以下是几种主流且经过生产验证的实现策略,从强一致性最终一致性排序。

核心原则:先更新数据库,再操作缓存

绝对不要先更新缓存,再更新数据库,如果数据库更新失败,缓存就变成了脏数据,几乎所有可靠的方案,都是以数据库的更新为基准。


Cache-Aside Pattern(旁路缓存模式)—— 最常用

这是大多数应用默认采用的模式,核心思想是应用同时维护数据库和缓存

  • 读请求
    1. 读缓存 -> 命中则直接返回。
    2. 未命中 -> 查数据库。
    3. 将查到的数据写入缓存。
    4. 返回数据。
  • 写请求(更新数据):
    1. 更新数据库。
    2. 删除缓存(而不是更新缓存)。

为什么写操作是“删除”而不是“更新”?

  • 延迟加载:只有下次读请求未命中时,才重新加载数据,相当于“懒更新”,这避免了更新缓存带来的计算开销(如复杂SQL查询结果)。
  • 避免并发问题:如果同时有两个写请求,先更新缓存的后更新数据库,会导致缓存是旧值,删除则不会有此问题。

并发问题及其解决方案(一种典型的“脏读”场景):

  • 场景:请求A写数据库并删除缓存后,请求B刚要读缓存,发现被删,于是查询数据库,但数据库更新可能还未完全生效(如主从延迟),请求B读到旧数据,并写入了缓存。
  • 解决方案
    1. “延迟双删”:更新数据库后,先删除一次缓存,延迟几百毫秒(等主从同步完成),再删除一次缓存,这是工程上的折中。
    2. “设置过期时间”:缓存必须设置一个合理的过期时间(TTL),即使临时脏读了,数据也会自动过期,最终达到一致,这是兜底策略

适用场景:读多写少、对一致性要求不是极端严格(最终一致性即可)的场景。


Read/Write Through Pattern(读写穿透模式)

缓存作为数据库的代理层,应用只与缓存交互,缓存负责同步数据到数据库。

  • 读请求:同上,缓存未命中时,缓存组件自动去数据库加载。
  • 写请求:应用更新缓存,缓存组件自动将数据持久化到数据库。

特点

  • 优点:应用代码最简洁,无需关心数据同步逻辑。
  • 缺点:实现复杂,需要开发或依赖成熟的缓存中间件(如Apache Ignite、Hazelcast),很多简单缓存(如Redis自己)不直接支持自动同步。

适用场景:数据模型简单、对缓存层有完全控制权的内部系统。


Write Behind Cache Pattern(异步回写模式)

这是“读写穿透”的变体,应用更新缓存后,不立即更新数据库,而是异步批量同步。

  • 写请求:更新缓存 -> 将变更任务写入消息队列或日志 -> 直接返回成功。
  • 后台任务:周期性或按策略批量将缓存变更应用到数据库。

特点

  • 优点性能极高(写请求响应极快),能将随机写合并成顺序写或批处理,减轻数据库压力。
  • 缺点数据一致性风险极高,如果缓存或后台服务崩溃,未同步的数据会丢失(除非有持久化机制),数据库和缓存不再是实时一致。

适用场景:对数据一致性要求不高,但对写性能有极致需求的场景(如计数器、访问日志、排行榜)。


基于 Binlog/CDC(变更数据捕获)的最终一致性方案

这是目前互联网大厂处理高并发、高一致性场景的首选方案,核心思想是完全解耦应用代码和缓存同步逻辑。

  • 应用A(数据生产者):只负责更新数据库,对缓存的存在无感知
  • 数据库:产生Binlog(如MySQL的binlog)或WAL(预写日志)。
  • 同步组件(如Canal、Debezium、Maxwell):订阅并解析数据库的变更日志。
  • 消息队列(可选,推荐):同步组件将变更事件发送到MQ(如Kafka、RocketMQ)。
  • 应用B(缓存消费者):消费MQ消息,根据变更ID/类型,查询新数据并更新或删除缓存。

为什么好?

  1. 彻底解耦:业务代码不必写任何“先写库再删缓存”的逻辑,只需要更新数据库。
  2. 高可靠性:Binlog是数据库的原始日志,生产者不会丢失数据,MQ保证了数据传输。
  3. 并发安全:天然解决了“先读后写”的并发问题,因为同步是按照数据库的提交顺序执行的(。
  4. 可审计:所有变更都有日志可追溯。

适用场景:对一致性要求高、并发量大、团队有能力搭建维护数据管道的核心业务(如订单、支付、用户中心)。


各策略对比总结

策略 一致性等级 性能 实现复杂度 适用场景
Cache-Aside 最终一致性 标准业务(读多写少)
Read/Write Through 强一致性(缓存层实现) 有自研缓存中间件的场景
Write Behind 弱一致性 极高 日志、计数器、排行榜
Binlog/CDC 强最终一致性 高并发、高一致性核心业务

两种“坑”与最佳实践

  1. 不要使用“先删缓存,再更新数据库”

    • 场景:A删缓存 -> B读缓存失败,读旧数据入缓存 -> A更新数据库。
    • 结果:缓存永远是旧数据,永远不要用这个顺序。
  2. 缓存必须设置过期时间(TTL)

    • 这是数据一致性的最后防线,即使所有同步机制都出问题(如程序bug、网络故障),缓存也会在TTL后自动失效,从数据库重新加载。
    • 建议:TTL不宜过长(如秒级或分钟级)。

推荐方案(针对不同阶段)

  • 小型/初创项目Cache-Aside + 延迟双删 + 较短的TTL,简单、有效、成本低。
  • 中型/标准项目Cache-Aside + 可靠的MQ(如RabbitMQ),在写操作后,发送一条MQ消息,消费端去删除缓存,平衡了一致性和复杂度。
  • 大型/核心项目Binlog/CDC(Canal/Kafka)+ Cache-Aside,彻底解耦,由专门的基础设施团队维护数据管道。

没有银弹。 你需要根据业务对数据不一致的容忍度团队的技术能力系统架构来权衡,大多数场景下,先更新数据库,再删除缓存,并设置合理的TTL 是最稳健的起点。

抱歉,评论功能暂时关闭!