tyltr技术窝

系统到达一定并发量之后,就需要考虑缓存和数据库一致性的问题。
注:本文中所有的缓存都是有失效时间的。

1. 使用缓存的基本流程#

缓存基本流程

  • 先在缓存中获取,命中缓存,则返回数据。
  • 如果未命中缓存,则再数据库中查找,如果没有查找到直接返回响应
  • 如果在数据开查询到数据,则写入缓存(设置缓存的失效时间),在返回响应

在高并发场景下,如果在缓存失效之前,只进行读操作(不修改数据库数据),不会产生数据一致性问题。
如果进行了写操作的话,有可能会找出数据一致性的问题。

2. 数据一致性问题分析#

在高并发写数据的时候,我分析一下缓存和数据库更新的几种方案:

  • 1.先更新缓存、再更新数据库
  • 2.先更新数据库,再更新缓存
  • 3.先删除缓存,再更新数据库
  • 4.先更新数据库,再删除缓存

上面的方案,两种是更新缓存、两种是删除缓存。下面我们分析一下。

方案一:先更新缓存、再更新数据库#

这种方案首先要被排除。因为不能确定数据库数据是否更新成功。万一缓存更新了,数据库更新失败,数据不一致。

方案二:先更新数据库,再更新缓存#

在数据库更完成的前提下,更新缓存,避免了方案一的问题。但也会引来新的问题。

问题一:在高并发场景下,请求A、请求B同时发出更新数据库的请求。
假设数据库内字段X的值为3,请求A执行X+3,请求B执行X-1。那么会出现

请求A 请求B 数据库 缓存 过程
3 3 原始数据
+3 6 3 A更新数据库
-1 5 3 B更新数据库
5 5 B更新缓存
5 6 A更新缓存

在上面表述的流程,因为网络,系统调度等问题,会找出一些脏数据。这种方案也是普遍反对的。

方案三:先删除缓存,再更新数据库#

这种也会造成数据的不一致性。还是两个请求A、B。A是写操作请求,B是读操作请求。流程如下

  • 1、请求A先删除缓存
  • 2、请求B未命中缓存
  • 3、请求B查询数据库的到旧数据
  • 4、请求B将获取的旧数据写入到缓存中
  • 5、请求A更新数据库

注:4、5互换亦可
上述情况会导致不一致的情况出现。针对这种方案曾提出解决方案延时双删除
其实就是在前5步骤的基础上,再加上一步。

  • 6.在请求A更新完数据库后,延时一定时间,再次删除缓存数据。

这种策略虽然能最终一致,但是在第6步之前依旧是不一致情况。而且具体延时不易操作,而且还有一个致命问题。
就是数据库主从分离(高并发下,主从分离是普遍的方案),第3步在从库查询数据,也是旧数值。这其中还要有主从
同步的时延。第三种方案也不能解决问题。

方案四:先更新数据库,再删除缓存#

这种在逻辑上也有漏洞的。就是在缓存失效的时候。还是两个请求A、B。A是写操作请求,B是读操作请求。

  • 1.缓存失效
  • 2.B在数据库中查找旧数据
  • 3.A更新数据库
  • 4.删除缓存
  • 5.B把旧值写入缓存

这样也造成了数据的不一致,理论上是的。但是你有没发现一个问题呀,上面的流程中两次写数据的时间竟然小于一次读数据的时间
即:A更新数据库+删除缓存的时间 < B读取旧数据的时间。在现实中,这种可能性发生的概率微乎其微。
好像解决的,但是万一更新缓存失败了怎么办?只能进行重试了。

普遍解决方案#

目前在高并发解决方案如下,我们可采用阿里开源的canal中间件来解决。流程如下:

流程如下

说明:这也是基于’先更新数据库,再删除缓存’的逻辑,只不过采用canel实现binlog的订阅。另外,采用消息队列实现异步的重试和人工干预。

虽然上面的很多案例都有各自的优缺点,但是需要根据业务进行选取适宜的方案。

参考#

  • 沈剑大佬