栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

缓存与数据库数据一致性问题详细分析及解决方案

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

缓存与数据库数据一致性问题详细分析及解决方案

目录
  • 解决此问题主要思路
  • db与cache操作过程可能产生的问题及解决方案
    • 问题一:并发问题,数据不一致产生的主要原因(应用层问题)
      • 根本问题
      • 双写模式问题
      • 失效模式问题
    • 问题二:第一步操作成功,但第二步操作可能失败(应用层问题)
      • 必须先更新db
      • 如何更新/删除cache
      • 如何重试
      • 异步重试的方式是否会对双写模式和失效模式产生影响
    • 问题三:db主从复制延迟导致的数据不一致(db层问题)
      • 延迟双删解决方案
    • 两种方案综合分析
      • 双写模式
      • 失效模式
      • 结论
    • 最佳实践
      • 失效模式方案二:订阅数据库变更日志方案
      • 是否有必要追求强一致性(各种场景下百分百保证一致性)
      • 实践原则
      • 具体实践方案
  • 参考文献


解决此问题主要思路

同时操作数据库和缓存,使二者达成数据一致。主要的实现方式有以下两种

  1. 双写模式

    改db(save,update,del),同时改一下cache

  2. 失效模式(推荐)

    改db,同时删除对应cache,读取数据时再更新cache。更新cache的方式有两种,分别是手动更新与自动更新,以下主要考虑在应用层主动更新的情况,使用binlog自动更新cache的情况作为一种独立方案讨论。

但是以上两种操作方式在操作时会存在一些情况,下面具体展开分析。

db与cache操作过程可能产生的问题及解决方案 问题一:并发问题,数据不一致产生的主要原因(应用层问题) 根本问题

db的修改与cache的修改无法保证原子性

双写模式问题

如果出现多线程同时修改db的情况,网络延迟导致数据修改顺序与请求顺序错位,会导致cache修改错误。

如:AB两thread同时对数据进行更新,A先更新,B后更新

序号正常逻辑异常逻辑:网络原因A执行慢,B执行快
1A写入db,db为AA写入db,db为A
2A写入cache,cache为AB写入db,db为B
3B写入db,db为BB写入cache,cache为B
4B写入cache,cache为BA写入cache,cache为A
结果db中为B,cache中为Bdb中为B,cache中为A

出现此问题前提条件

  1. 双写并发
  2. 1最早发起写入的A thread 的更新时间变长
失效模式问题

还是网络延时问题,导致db和cache操作非原子性

如:AB两thread同时对数据进行更新,C正常读取数据,A先更新(满足cache为空的前提),B后更新,C发起读取。

序号正常逻辑异常逻辑:网络原因A执行慢,B执行快
1A写入db,db为AA写入db,db为A
2A删除cache ,cache为nullA删除cache ,cache为null
3B写入db,db为BC读取cache,cache为null
4B删除cache ,cache为nullC去db拉数据,拉到A
5C读取cache,cache为nullB写入db,db为B
5C去db拉数据,拉到BB删除cache ,cache为null
6C更新cache,cache为BC更新cache,cache为A
结果C读取结果为B,db为B,cache为BC读取结果为A,db中为B,cache中为A

出现此问题前提条件

  1. 只有读写并发(cache初始为null,B写thread、C读thread并发)才会出现
  2. C thread读取数据时,cache刚好为null
  3. C读数据的过程中,刚好有B thread在修改db
  4. B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间
问题二:第一步操作成功,但第二步操作可能失败(应用层问题) 必须先更新db

先更新/ 删除 cache,后更新db方案不可用,因为第二步db更新可能不成功。此时如果cache更新成功,但db更新失败,那么此时cache中是新值或null,但db中是旧值。只要cache过期失效,就会从db中读取到旧值,数据更新失败

结论:无论使用双写模式还是失效模式,只能先更新db,后更新/删除cache

如何更新/删除cache
  1. 正常写cache。

  2. 如果更新cache失败,则需进行重试。

如何重试
  1. 如果当前线程直接重试,问题在于
  • 立即重试很大概率「还会失败」
  • 「重试次数」设置多少才合理?
  • 重试会一直「占用」这个线程资源,无法服务其它客户端请求
  • 如果此时项目「宕机」了,待更新数据直接丢失
  1. 更好的方案建议采用异步重试,把重试请求写到「MQ」中,然后由专门的消费者来重试
  • 重试时间和次数由当前线程控制
  • 重试交给mq,可以释放当前线程资源
  • MQ本身可靠性,可以保证数据不丢失

异步重试的方式是否会对双写模式和失效模式产生影响

参考问题一中的问题场景

  1. 双写模式:当A、B两thread顺序修改db和缓存,均采用先写db后写cache的方式更新,此时若A写cache出现失败重试,在完成重试前,B thread很可能已经完成了db和cache两步的更新,所以采用异步的方式对双写模式影响较大
  2. 失效模式:失效模式问题案例中A thread存在的意义在于创造特定初始值(cache初始为null,db值为A),所以只需分析B thread和C thread关系即可,C thread由于是read thread 所以不存在更新db成功,更新cache失败的场景,所以不会造成数据不一致,唯一的修改db和cache的只有B thread。而失效模式出现数据不一致的场景,只有当B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间才会出现,使用MQ本身就拉长了B更新db时间 + 删除cache的时间进一步避免了数据不一致场景的出现。

针对问题一和问题二分析,综上使用失效模式更有利于天然保障数据一致性

问题三:db主从复制延迟导致的数据不一致(db层问题)

问题三考虑是纯db层的问题,此问题独立于问题一和问题二出现,当应用保证数据一致性后,在db层仍可能出现问题,此问题无论是使用双写模式,还是失效模式都有可能出现

如:A thread对数据进行更新,B thread读取数据,db值原始为C

序号正常逻辑异常逻辑:网络原因A执行慢,B执行快
1A写入db「主库」, 主库db为AA写入db「主库」, 主库db为A
2A删除cache ,cache为nullA删除cache ,cache为null
3B读取cache,cache为nullB读取cache,cache为null
4B去db「从库」拉数据,拉到AB去db「从库」拉数据,拉到C
5B更新cache,cache为AB更新cache,cache为C
结果db中为A,cache中为Adb中为B,cache中为C
延迟双删解决方案

由于前序问题已证明失效模式对于保障数据一致性更优,所以此处只分析失效模式下的延迟双删策略。

延迟双删策略即 A thread 可以生成一条「延时消息」,写到MQ中,消费者延时「删除」缓存。延迟时间理论上需保证

  • 延迟时间要大于「主从复制」的延迟时间。此时再删除cahce可以保证下次read 从db拉值的时候一定是新值

但是,这个时间在分布式和高并发场景下,其实是很难评估的,很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。

两种方案综合分析 双写模式
  1. 双写模式并发问题出现的前提,一般是双写并发。只要满足最早发起写入的A thread 的更新时间变长即会出现这种问题
  2. 双写模式会频繁更新cache,导致cache中存放了很多无用数据,cache利用率低
  3. 如果要保证不会出现类似问题,需要使用分布式锁(读写锁即可),使得最早一次A thread更新操作未全部完成前,另一B thread阻塞。但所有数据库更新全部变成单线程,此时会降低并发能力
失效模式
  1. 失效模式并发问题出现的前提,一般是读写并发。且要满足B更新db时间 + 删除cache的时间 < C读取db时间 + cache赋值的时间,但写db mysql会自动加锁,同时数据库多副本的情况下,更新数据需要主从同步,所以写时延>>>读时延。所以此种情况发生的概率极低
  2. 如果要保证不会出现类似问题,需要使用分布式锁(读写锁即可),使得最早一次A thread更新操作未全部完成前,另一B thread阻塞。但所有数据库更新全部变成单线程,此时会降低并发能力
结论

失效模式在非极端情况下,可以认为是天然满足db与cache缓存一致的。双写模式有较大概率出现数据不一致问题

最佳实践 失效模式方案二:订阅数据库变更日志方案

业务应用在修改数据时,「只需」修改数据库,无需操作缓存。当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,redis拿到具体操作的数据,然后再根据这条数据,去删除对应的cache。例如阿里的canal中间件

是否有必要追求强一致性(各种场景下百分百保证一致性)

强一致性解决方案,双写模式和失效模式都可以通过加读写锁的方式,保证数据一致性。但所有数据库更新全部变成单线程,极大降低并发能力,甚至很可能会超过引入缓存带来的性能提升

实践原则
  1. 经常性修改的数据,就不要放缓存,直接读数据库。避免频繁更新缓存
  2. 触发读更新时,设置缓存过期时间。保证缓存利用率
  3. 采用失效模式。天然满足数据一致性
  4. 根据需要选择手动删除cache还是自动删除cache,如果要手动删除cache,引入MQ参与重试机制重试可靠消息投递
具体实践方案

根据业务场景选择做一致性和性能的trade off

  1. 数据是否会有大并发

    如:用户操作维度的数据,并发可能性小。一般给缓存加过期时间,一段时间后触发读更新即可

  2. 如果数据会出现并发,考虑对用户重要性,客户读到过期数据是否会产生重要影响

    如:菜单栏、商品介绍等数据,是旧版还是最新版对使用者影响小,可使用canal订阅binlog方式

  3. 绝大部分场景解决方案:给缓存加过期时间即可,最多再考虑一下失效方案,分布式锁的方案大并发场景下不要用

参考文献

https://zhuanlan.zhihu.com/p/408515044

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/998905.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号