持久化数据&缓存数据双写一致性

  • Post author:
  • Post category:其他




背景

缓存中数据更新一般有两个入口,

  • 数据缓存过期,数据在访问时发现缓存中无数据时重新查库然后更新至缓存;

    • 场景和问题等同于缓存查询,相关solution参考“缓存数据查询的注意事项”;
  • 缓存未过期,数据库数据有变动主动更新至缓存;

    • 比较常见的场景;
    • 也即为双写的概念:有新版本的数据需要同时写入持久化层和缓存层中;



问题点



数据一致性

  • 是指数据库持久化数据与高速缓存库中缓存数据的不一致;
  • 一般倾向于数据库为准,也就是说不一致是指缓存数据的时效性保障性低于持久化数据;
  • 不一致也即为缓存数据为脏数据;



脏数据的概念

  • 缓存的更新过程可能会出现时间上或长或短的时效性过期情况(或者说无法避免);
  • 长期或者在缓存单位生命周期中是脏数据的情况是无法接受的;
  • 但是如果在实现级出现短暂的脏数据,通过兜底或者其他策略完整最终一致性是解决方向;



缓存的过期时间

  • 常见缓存过期时间的处理策略是

    • 设置指定的过期时间:经过过期时间之后在数据查询过程中绑定缓存初始化的流程实现更新;
    • 不设置过期时间:长期有效,只有主动发起才会执行缓存的数据更新;
  • 关于数据一致性

    • 上述的设置过期时间的策略也是对数据一致问题的一种强行兜底方案;

      • 只是兜底,并不能依赖,缓存单位生命周期长度的脏数据无法接受;
    • 需要将脏数据的存在时间范围放在可控范围内;

      • 可控范围:业务可接受的范围;



问题解析



排除因素

  • 在分析一致性问题时我们排除使用过期时间兜底实现一致性的策略;

    • 首先,只是最终兜底,并且业务不可接受;
    • 以不过期的使用背景来分析,问题更明显;



问题来源

  • 无事务:缓存的操作不被包含在事务控制中;

    • 典型的情况:缓存更新失败,但是不影响数据库事务的成功提交,导致不一致;
  • 并发:因为无事务,无法保证双写的一致性;

    • 并发导致的时序问题导致数据覆盖,可能出现有效数据是旧数据的情况;
  • 多数据源:如果拿着一份数据分别向持久层和缓存执行执行更新,在时序无法保证时会出现交叉覆盖的情况;
  • 互斥:缓存的初始化(查询数据+set缓存)需要保证原子性,并且初始化和缓存的淘汰需要互斥,否则并发下交叉执行会有旧数据覆盖情况;

多数据源的描述实际也是因为无事务+并发造成的;



解决方向

  • 唯一数据源:

    • 以持久化数据为准,由缓存依赖获取持久化数据来实现更新;
  • 事务、并发:

    • 虽无法将缓存操作加入事务,但是需要保证缓存的操作在数据库事务提交之后,保证缓存更新依赖的数据有效性;
    • 快速兜底:在出现异常情况时通过重试机制快速兜底或预警通知;
  • 保证互斥:

    • 缓存的清楚和初始化使用锁保证互斥;



方案



更新缓存

更新缓存or淘汰缓存(删除缓存)

根据上述的通过保证唯一数据源来避免问题,应该采用淘汰缓存,然后缓存通过从持久化层获取数据完成初始化;



代表方案

如果采用更新缓存,无论更新缓存和更新数据库前后顺序怎么排列;

即无论是“更新缓存+更新数据库”还是“更新数据库+更新缓存”都会有以下问题;



问题:

  • 并发:

    • 如果多线程执行时序发生交叉,即ABBA或者BAAB时,持久层由于有数据库事务的保证,数据最终是有效的,但是缓存可能会发生旧数据覆盖新数据的情况;
  • 事务:

    • “更新数据库+更新缓存”,假如“更新缓存“的环节失败,但是“更新数据库”事务已经提交,导致不一致,这个问题需要我们关注,包括后续方案也需要关注这个问题;
    • “更新缓存+更新数据库”,这个就会有致命问题,即新数据已经更新至缓存,但是“更新数据库”时失败了,反而导致缓存数据是未持久化的数据;



结论

使用淘汰缓存,而不是更新缓存;



“淘汰缓存+更新数据库”



问题:

  • 事务+并发:

    • 因为两个环节的操作无事务统一管理,没有原子性;

      • 则可能在淘汰缓存之后、更新数据库事务提交之前;由其他线程访问缓存数据,触发缓存刷新流程,此时持久层数据仍是旧数据,而且被刷新至缓存,导致不一致;



结论:

  • 无法保证一致性,隐患很大;



“更新数据库+淘汰缓存”



细节

  • 保证“淘汰缓存”在“更新数据”事务提交之后,即:保证持久层有新数据之后再执行缓存淘汰;



问题:

  • 事务+兜底:

    • 此时,如果持久层事务提交之后,淘汰缓存因为各种原因失败,导致不一致;



一个细节问题(关于缓存操作的互斥):

- 如果不考虑上述缓存操作异常情况;
- 更新缓存时如果缓存未超时,此场景是无问题的;
-  场景:如果缓存数据是有效期的或者其他操作,导致在执行“更新数据库”之前 原缓存数据过期,此时有其它线程访问缓存数据并触发了缓存初始化流程:
    A_查询持久化数据+B_将查询结果set至缓存;那么就会有一个细节上的问题了:
    - 如果上述两个环节A&B都在当前线程的C_淘汰缓存这个环节之前完成,那就不会有问题,最后结果是再次淘汰缓存,保证了一致性;
    - 如果上述两个环节A&B交叉在C之间,即执行时序是A-C-B,那么上述的C_淘汰缓存操作就无意义了,还是会导致不一致;
- 解决方案:
    - 因为缓存的初始化是有同步锁控制的,如果将缓存淘汰操作也加入同一个同步锁控制内,就可以避免上述A-C-B的问题;并且不会造成额外影响;



结论:

  • 逻辑上和可行性个人认为是最靠谱的一个;

    • 通过消息支持重试机制进行兜底;
    • 通过同步锁与缓存初始化互斥保证逻辑完整;
  • 逻辑上是符合常理的;并且数据持久化完成之后“更新缓存”操作失败的兜底机制和保证缓存的互斥性是需要关注的核心问题;



数据库主从同步延迟的问题



问题描述:

上述的数据持久化都是在讲的概念,实际上如果存在读写分离,从库从主库同步数据需要一定的时间,即使在保证上述逻辑正常执行的情况下,

也需要考虑,当新数据成功持久化至主库之后、从库同步数据之前,有访问数据的请求落在从库上,读到的还是旧数据,仍然会造成脏数据;



解决思路:

订阅数据库binlog日志,在数据完成同步的时候重新发起“淘汰缓存”操作,并辅以消息重试机制保证执行;



版权声明:本文为MikeLC7原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。