如何保持mysql和redis中数据的一致性?

1、流程图说明

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

2、疑问

在缓存和数据库同时存在时,如果有写操作的时候,先操作数据库还是先操作缓存呢?先思考一下,可能会存在哪些问题,再往下看。

3、更新策略

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

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

3.1.1 原因一(线程安全角度)

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

  • 1)线程A更新了数据库
  • 2)线程B更新了数据库
  • 3)线程B更新了缓存
  • 4)线程A更新了缓存

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

3.1.2 原因二(业务场景角度)

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

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

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

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

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

  • 1)请求A进行写操作,删除缓存
  • 2)请求B查询发现缓存不存在
  • 3)请求B去数据库查询得到旧值
  • 4)请求B将旧值写入缓存
  • 5)请求A将新值写入数据库

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

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

/**
*解决方法的伪代码
*/
public void write(String key,Object data){
	//1、先删除缓存
	redis.delKey(key);
	//2、更新数据库,写入数据
	db.updateData(data);
	//3、休眠1秒
	Thread.sleep(1000);
	//4、再次删除缓存
	redis.delKey(key);

}

3.2.1 休眠时间如何确定?

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

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

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

首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

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

另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。

/**
*解决方法的伪代码
*/
public void write(String key,Object data){
	//1、更新数据库,写入数据
	db.updateData(data);
	//2、删除缓存
	redis.delKey(key);
}

3.3.1 这种情况不存在并发问题么?

不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

  • 1)缓存刚好失效
  • 2)请求A查询数据库,得一个旧值
  • 3)请求B将新值写入数据库
  • 4)请求B删除缓存
  • 5)请求A将查到的旧值写入缓存

ok,如果发生上述情况,确实是会发生脏数据。

3.3.2 发生并发问题的概率是多少?

发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。

3.3.3 如何解决上述并发问题?

首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作

4、最佳解决方案讨论

4.1 方案一

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

4.2 方案二

Redis里的数据总是不过期,但是有个背景更新任务(“定时执行的代码” 或者 “被队列驱动的代码)读取db,把最新的数据塞给Redis。这种做法将Redis看作是“存储”。访问者不知道背后的实际数据源,只知道Redis是唯一可以取的数据的地方。当实际数据源更新时,背景更新任务来将数据更新到Redis。这时还是会存在Redis和实际数据源不一致的问题。如果是定时任务,最长的不一致时长就是更新任务的执行间隔;如果是用类似于队列的方式来更新,那么不一致时间取决于队列产生和消费的延迟。常用的队列(或等价物)有Redis(怎么还是Redis),Kafka,AMQ,RMQ,binglog,log文件,阿里的canal等。

5、参考资料

https://www.zhihu.com/question/319817091
https://zhuanlan.zhihu.com/p/98909029

猜你喜欢

转载自blog.csdn.net/qq_37493556/article/details/107667185