简单谈谈缓存与数据库双写一致性保证策略

Posted 分布式朝闻道

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单谈谈缓存与数据库双写一致性保证策略相关的知识,希望对你有一定的参考价值。

昨天的每日一题,提出了关于缓存与缓存一致性如何保证的问题。私以为,这个问题对于日常开发和技术进阶比较重要,值得专门写一篇文章进行讲解。那么,本文我们就这个问题来进行详细的分析。


问题如下:

缓存与数据库双写一致性是如何保证的?具体聊聊Cache Aside Pattern?还有别的策略吗?


稍安勿躁,我们开始分析。


缓存更新策略通常有四种,分别为:

  • Cache aside

  • Read through

  • Write through

  • Write behind caching


其中Cache Aside Pattern是最为常用的,它的核心操作概括一下就是 

  • 新增操作,写数据库成功,直接返回

  • 更新操作,更新数据库成功,失效缓存

  • 查询缓存,命中缓存直接返回;未命中,查数据库,查找成功,更新缓存,返回查询结果。


具体的执行过程为:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

  • 命中:应用程序从cache中取数据,取到后返回。

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。


用图形表示如下,更为直观:



注意,我们的更新是先更新数据库,数据库更新成功后,再让缓存失效。那么,这种方式是否能够保证数据库和缓存数据的一致性,保证缓存中没有脏数据呢?具体分析如下:


假设有两个线程并发执行两个操作,一个是查询操作,一个是更新操作。

首先更新数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新之前的数据。但是,当更新操作完成之后,马上失效了缓存;当后续的查询操作再把数据从数据库中查询出来。取到的就是新数据,并且会把新数据更新到缓存中,并不会出现后续的查询操作一直都在取老的数据。(如果我们不按照这个模式执行操作,而是先删除缓存,再更新数据库,寄希望于后续的操作把数据再装载的缓存中。这个操作实际上会导致并发操作时,先查询出老数据的请求更新了缓存,导致后续的请求取到的始终都是老数据,从而出现缓存与数据的不一致性。


同理,如果不按照该模式进行操作,而是写完数据库后直接更新缓存,这也会导致缓存与数据库数据出现不一致,愿意也很好理解,就是两个并发的写操作可能导致先更新数据库的请求后更新缓存,导致数据库中的数据已经更新了,但是缓存中的数据却不是最新更新的数据,导致缓存了脏数据。


当然,Cache Aside模式也有其局限性。比如,一个读操作,当没有命中缓存,它会到数据库中取数据;此时来了一个并发写操作,当该写操作写完数据库后,让缓存失效。

接着,之前的读操作再把老的数据放进去,还是会造成脏数据。但是实际上,这个情况在真实场景,几乎不会出现,原因在于这个条件需要发生在读缓存时出现缓存失效,而且还并发存在有一个写操作。


而实际上数据库的写操作会比读操作慢得多,而且还会对记录加锁,而读操作必须在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率是一个非常苛刻的小概率事件,因此我们认为Cache Aside Patten是一个很好的实现数据库与缓存数据一致性的策略。



其他策略

除了Cache Aside Pattern外,再介绍一种策略,基于数据库Binlog对缓存进行异步刷新,近乎实时地更新缓存。


具体流程,如下图:


针对图中的流程,详细描述一下这个过程是如何工作的。


  1. 首先,接口或者请求调用会实时获取并解析上游对数据库中数据的修改参数;

  2. 通过对应的DML操作SQL对数据进行修改,插入,删除等操作。这些操作会生成对应的binlog记录;

  3. 通过binlog订阅工具,如阿里开源的Canal等工具,订阅数据库binlog,对其进行解析,获取需要与缓存进行同步的binlog,并对其进行转换;

  4. 通过缓存更新逻辑,对解析后的binlog中对缓存执行更新、失效等修改操作。


总的来说,这个过程是一个基于最终一致性的策略,符合CAP理论中的AP系统,本质上,关系数据库与缓存间的数据一致性问题是一个分布式事务问题,如果要从根源上彻底解决这个问题,需要用到两阶段提交、三阶段提交等重型强一致分布式事务思想,但实际中往往不需要引入如此复杂重量级的策略。


此处介绍的binlog订阅刷新缓存方式,具有较好的实时性,而且业务逻辑对缓存的更新是无感知的,只需要执行缓存的查询和数据库中数据的插入、更新(包括删除)等操作。


该方案的优点和缺点总结如下:


方案优点
  1. 对缓存的更新操作,不再需要显式编写,从而使代码更加内聚简洁;

  2. 不需要显式失效缓存,能够在高并发场景下尽量减少缓存击穿问题;并且由于减少了显式的缓存失效操作,一定程度上降低了接口的处理时延,降低了rt,提高了调用效率;


方案缺点
  1. 引入了binlog解析组件,使业务复杂度增加,需要强依赖binlog订阅组件的可用性;如果业务量较大,还需要引入消息中间件作为缓冲层,进一步增加了系统可用性与延迟.

  2. 虽然binlog订阅实时性较好,但是由于需要严格保证顺序性,因此在binlog消息较多的情况下,由于消息积压会造成数据延迟性与不一致概率增加

总的来说,没有最好的方案,只有最适合的方案,脱离了业务谈设计都是“耍流氓”。

作为技术人,知道为什么要用,比知道怎么要用更为重要。

知其然更要知其所以然,思路比方法本身更为重要,温故而知新,我们下个问题见。


明日问题:

分库分表之后,id主键如何处理?(本问题由 @Howe 同学提出并提供参考答案,特此鸣谢)


以上是关于简单谈谈缓存与数据库双写一致性保证策略的主要内容,如果未能解决你的问题,请参考以下文章

场景应用:如何保证缓存与数据库的双写一致性?

Redis与Mysql双写一致性

Redis与MySQL双写一致性如何保证?

美团二面:Redis与MySQL双写一致性如何保证?

美团二面:Redis与MySQL双写一致性如何保证?

美团二面:Redis与MySQL双写一致性如何保证?