图片 3

分布式之数据库和缓存双写一致性方案解析

Posted by

引言

引言

为什么写这篇文章?

为什么写这篇文章?

首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作。

首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作。

图片 1

图片 2

但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存。又或者是先删除缓存,再更新数据库,其实大家存在很大的争议。目前没有一篇全面的博客,对这几种方案进行解析。于是博主战战兢兢,顶着被大家喷的风险,写了这篇文章。

但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存。又或者是先删除缓存,再更新数据库,其实大家存在很大的争议。目前没有一篇全面的博客,对这几种方案进行解析。于是博主战战兢兢,顶着被大家喷的风险,写了这篇文章。

文章结构

正文

本文由以下三个部分组成

先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。

1、讲解缓存更新策略

在这里,我们讨论三种更新策略:

2、对每种策略进行缺点分析

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

3、针对缺点给出改进方案

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

正文

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

先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。

应该没人问我,为什么没有先更新缓存,再更新数据库这种策略。

在这里,我们讨论三种更新策略:

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

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

这套方案,大家是普遍反对的。为什么呢?有如下两点原因。

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

原因一

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

同时有请求A和请求B进行更新操作,那么会出现

应该没人问我,为什么没有先更新缓存,再更新数据库这种策略。

线程A更新了数据库

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

线程B更新了数据库

这套方案,大家是普遍反对的。为什么呢?有如下两点原因。

线程B更新了缓存

原因一

线程A更新了缓存

同时有请求A和请求B进行更新操作,那么会出现

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

线程A更新了数据库

原因二

线程B更新了数据库

有如下两点:

线程B更新了缓存

如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

线程A更新了缓存

如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

原因二

先删缓存,再更新数据库

有如下两点:

该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

请求A进行写操作,删除缓存

如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

请求B查询发现缓存不存在

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

请求B去数据库查询得到旧值

先删缓存,再更新数据库

请求B将旧值写入缓存

该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

请求A将新值写入数据库

请求A进行写操作,删除缓存

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

请求B查询发现缓存不存在

那么,如何解决呢?采用延时双删策略

请求B去数据库查询得到旧值

伪代码如下

请求B将旧值写入缓存

图片 3

请求A将新值写入数据库

转化为中文描述就是

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

先淘汰缓存

那么,如何解决呢?采用延时双删策略

再写数据库

伪代码如下

休眠1秒,再次淘汰缓存

1

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

2

那么,这个1秒怎么确定的,具体该休眠多久呢?

3

针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

4

如果你用了mysql的读写分离架构怎么办?

5

ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

6

请求A进行写操作,删除缓存

public void write(String key,Object data){

请求A将数据写入数据库了,

redis.delKey;

请求B查询缓存发现,缓存没有值

db.updateData;

请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值

Thread.sleep;

请求B将旧值写入缓存

redis.delKey;

数据库完成主从同步,从库变为新值

}

上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

转化为中文描述就是

采用这种同步淘汰策略,吞吐量降低怎么办?

先淘汰缓存

ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。

再写数据库

第二次删除,如果删除失败怎么办?

休眠1秒,再次淘汰缓存

这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

请求A进行写操作,删除缓存

那么,这个1秒怎么确定的,具体该休眠多久呢?

请求B查询发现缓存不存在

针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

请求B去数据库查询得到旧值

如果你用了mysql的读写分离架构怎么办?

请求B将旧值写入缓存

ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

请求A将新值写入数据库

请求A进行写操作,删除缓存

请求A试图去删除请求B写入对缓存值,结果失败了。

请求A将数据写入数据库了,

ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。

请求B查询缓存发现,缓存没有值

如何解决呢?

相关文章

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注