「微服务」缓存、数据库双写一致性

前言

缓存由于高并发和高性能的特性,在当前的主流架构中基本上属于必须的其中一个高并发支撑模块。缓存在使用时,读流程基本保持一致的认知,不存在其他异议。流程如下:

image

缓存读流程

但是,在写流程上,由于需要同时将数据写入数据库与缓存中,就一定涉及到缓存与数据库数据双写,数据一致性的问题。所以,在系统的设计上,需要考虑如何处理数据一致性问题。

一般来说,在引入缓存机制的同时也牺牲了一定的一致性,缓存数据与数据库数据是允许在一定时间窗口之间内的非一致性。如果必须保持一致的话,那么缓存的引入基本上没有意义了。必须保证强一致的话,就只能使用读请求和写请求串行化,串到一个内存队列里去。但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

常用双写模式解析

#先更新数据库,再删除缓存(Cache Aside)

Facebook使用的方式,也是推荐的使用方式。包括:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中;* 命中:应用程序从cache中取数据,取到后返回;* 更新:先把数据存到数据库中,成功后,再让缓存失效;
image

采用CacheAside模式的问题在于,在读写线程同时并发,且满足

  1. 读线程读取缓存时,缓存正好失效
  2. 读线程读数据库数据
  3. 写线程更新数据库
  4. 写线程删除缓存
  5. 读线程将查询到的数据写入缓存

以上所有查件时,在理论上来说,确实可能会产生脏数据。但是实际上出现的概念极低。

image

首先,需要满足读缓存时缓存失效,并且存在并发写。在实际上,数据库的写操作完比读操作慢得多,而且还需要锁表,而读操作必须在写操作前进入数据库操作,而又晚于写操作更新缓存,所有条件具备的概率太低,基本上忽略。

#先删除缓存,再更新数据库

image

先更新数据库,再删除缓存区别只是切换了执行的顺序。但是,该方式会导致大概率的不一致性发生。

  1. 写线程更新数据,删除缓存
  2. 读线程查询缓存数据,发现没有缓存数据
  3. 读线程读数据库数据
  4. 写线程更新数据库
  5. 读线程将查询到的数据写入缓存
image

解决方案:采用双删机制。删除缓存 > 更新数据库 > 休眠一点时间 > 再次删除缓存。

但是该方案不采纳。休眠时间无法评估,而且大大降低了系统吞吐。

#先更新数据库,再更新缓存

该方案是不能被采用的方案。

image

如上图所示,写线程A和线程B同时更新数据,但是线程B早于线程A更新缓存,导致数据不一致。

除此之外,缓存数据可能是多纬度的计算数据而不仅仅只是单一的某个数据。如某缓存数据由A表,B表,C表多表数据计算而成,那么缓存的更新因素就非常多的,而且要更新缓存,需要额外的查询其他表数据。

删除缓存是一种懒计算,在需要的时候才去重新计算。

推荐使用模式

在上述的模式中,推荐采用先更新数据库,再删除缓存(Cache Aside)模式。

但是存在一个问题,如果删除缓存的时候失败了如何处理?

在生产中,缓存的使用依赖于抽象缓存接口,根据允许的数据差异时间窗口和业务,采用的是针对实际缓存进行包装,提供删除重试机制,业务层不需要额外关注删除问题,避免了业务侵入。

重试机制采用的是本地重试。 根据配置的重试上限进行重试,达到上限无法删除成功抛出异常到上层。上层业务决定是否回滚修改,继续向上抛出异常,然后业务调用者的重试机制重新尝试。还是忽略该失败。

正常情况下,由于缓存本身影响的一致性导致数据一定存在一定时间窗口的差异。如果缓存在特定某一时刻无法删除,正常情况下不会影响业务,这是被允许的情况(缓存的数据必须有过期时间,而且时间不能太久,根据业务设置合理过期时长是非常重要的)。如果出现批量的失败,可能是缓存系统异常或者业务系统异常都会被监控系统探测到(监控系统很重要),此时已经不是某个业务或服务的问题,而是整个系统问题。

当然,如果非要强制一定要执行成功,建议方案使用MQ进行异步化。

image

总结

缓存的使用时双写是无法很好的保证一致性的,在使用缓存时就应该接受这一点,而不是强求一致性,导致系统或者业务变得非常复杂。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容