关于数据库和缓存一致性问题的思考

前言

在项目中,因为缓存具有高性能、高并发的特性而被大家广泛的使用。但是常常我们会遇到一个问题:当需要更新数据库的时候怎么保证缓存里的数据和数据库里面的数据始终保持一致呢?

这个问题就是著名的数据库和缓存的双写一致性问题

为了不让内容显得空洞,帮助一些没有对此问题没有一点直观概念的读者,建立起对该问题的直观概念。我先赘述一下问题发生的情况

下面代码是我们在项目中最常见的缓存的使用方式:

Object cache = redisTemplate.getValue(key);
if(Objects.nonNull(cache){
    return cache;
}
Object result = dbService.queryDb();
redisTemplate.putValue(cache);
return result;

流程图如下:
在这里插入图片描述

但是有时候,我们需要更新数据库的数据,而且这些数据在缓存中也存在,我们该怎么办呢?

什么情况下会导致缓存不一致呢?

我们可以做一个简单的排列组合,把所有能实现的方式列举出来然后在逐一采用画图和伪代码的方式讨论各种方案的可行性

无非以下几种方案:

  • 先更新缓存,后更新数据库
  • 先更新数据库,后更新缓存
  • 先删除缓存,后更新数据库
  • 先更新数据库,后删除缓存

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

伪代码

  //1、更新缓存
  int id = 3;
  redisService.update(3,"张三"));
  //2、更新数据库
  dbService.update(3,"张三");

这种情况显而易见,该方式不可行,原因:如果①更新缓存成功,将缓存里id为3的数据改为了张三,但是这个时候由于各种原因②来不及执行或者执行失败,那么数据库中的id为3的数据没有被改为张三,而用户查询的数据是张三,这就造成了数据不一致

在这里插入图片描述

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

伪代码

  //1、更新数据库
  int id = 3;
  dbService.update(3,"张三");
  //2、更新缓存
 redisService.update(3,"张三"));

这种情况也是显而易见的,该方式不可行(设置缓存过期时间除外)。原因:
① 更新数据库事务提交成功,由于各种更新缓存失败,并且该缓存没有设置过期时间,那么用户读取的数据将一直是旧数据。这也会造成数据不一致

在这里插入图片描述

② 开启数据库更新事务未提交,更新缓存成功后事务提交失败,如果未设置缓存的过期时间,那么用户将会一致读取脏数据。这也会造成数据的不一致

在这里插入图片描述

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

伪代码

// 1、删除缓存
int  id = 3;
redisService.delete(3)
// 2、更新数据库
dbService.update(3,"张三");

该方案需要讨论读写串行执行和读写并发执行两种情况:

串行执行

该方式在串行执行的情况下,讨论如下:

① 删除缓存成功,更新数据库失败;业务出错,下次用户查询的时候,仍然是从数据库中最新的数据,所以缓存和数据库数据一致
② 删除缓存失败;业务出错,缓存中的数据仍然是旧数据,而且数据库中的数据没有被更新也是旧数据,所以缓存和数据库数据一致
③ 删除缓存成功,更新数据库成功;数据一致

综上所述:先删除缓存,后更新数据库的方案在串行执行的情况下,能保证数据的一致性

并发执行

该方式在并发执行的情况下,讨论如下:

① 线程A删除缓存成功,线程B在线程A更新数据库之前,查询了数据库,将旧数据"李四",存入缓存,线程A再更新数据库数据为"张三",这时数据库和缓存的数据不一致
在这里插入图片描述

② 线程A删除缓存成功,线程B在线程A更新数据之后,查询数据库。线程B查询的将会是最新的数据,然后在数据放入缓存中,这时数据库和缓存中的数据一致
③ 线程A删除缓存失败,业务出错就不会执行更新数据库操作,缓存和数据库中的数据仍然是旧数据,数据是一致的

综上所述:先删除缓存,后更新数据库的方案在并发执行的情况下,能保证不能保证数据的一致性

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

伪代码

// 1、更新数据库
int  id = 3;
dbService.update(3,"张三");
// 2、删除缓存
boolean result = redisService.delete(3);
if(!result){
  抛出异常
}

该方案需要讨论读写串行执行和读写并发执行两种情况:

串行执行

该方式在串行执行的情况下,讨论如下:

① 更新数据库成功,删除缓存成功;数据一致
② 更新数据库失败,业务出错,不会删除缓存,数据库和缓存中的数据仍然是旧数据,数据保持一致
③ 更新数据库成功,删除缓存失败,这时可以通过数据库事务回滚。保证了的数据的一致性

综上所述:先更新数据库,后删除缓存的方案在串行执行的情况下,能保证数据的一致性

并发执行

该方式在并发执行的情况下,讨论如下:

① 线程A开启更新数据事务,在删除缓存成功之后事务未提交之前此时线程B查询数据,发现缓存为空,此时由于线程A未提交事务,查询数据库中的数据为旧数据,将旧数据放入缓存中后线程A提交事务修改数据库为新数据,就导致了数据不一致

Begin
updateDb
delete redis success
commit

在这里插入图片描述

② 线程A开启更新数据事务,在删除缓存失败之后事务为回滚事前此时线程B查询缓存中的旧值,数据库回滚后也是旧值。数据一致

Begin
updateDb
delete redis error
roback

在这里插入图片描述

③ 线程A开启更新数据事务并且提交成功,删除缓存失败。造成数据库和缓存数据不一致

Begin
updateDb
commit
delete redis error

在这里插入图片描述

综上所述:先更新数据库,后删除缓存的方案在并发执行的情况下,不能保证数据的一致性

解决方案

由第二部分的讨论结果,可以率先排除前两种操作方案及先更新数据库,后更新缓存先更新缓存,后更新数据库,因为对于串行执行而言都不能保证数据的一致性,从理论的角度上排除该方案。那么下面重点从后两种方案讨论一下解决方案。

设置缓存超时时间

这种方法是通用的也是最简单,但是如果过期时间设置过长会导致数据的不一致性时间延长;如果设置的过短,又会频繁的查询数据库,使缓存形同虚设。

分布式锁(读写串行化)

无论是先删除缓存,后更新数据库;还是先更新数据,后删除缓存;在串行执行的情况下都是能保证数据的一致性。所以我们就是要实行读写串行化

分布上场景下,最常用的便是分布式锁的方案,伪代码如下:

写线程:

加锁(key)
删除缓存
更新数据库
释放锁

读线程

加锁(key)
查数据
释放锁

这种解决方案,又引入了新的概念(分布式锁),我们知道程序中加锁是一件非常消耗性能的操作,与我们引入缓存来提升性能的初衷背离。但是它能保证数据的强一致性。

在对数据的一致性要求很高,但是能牺牲性能的情况下的可以使用。

异步补偿

针对先更新数据库,后删除缓存的方案的第三种代码顺序,可以采用异步补偿的方式实现最终一致性

在这里插入图片描述

该方案通过异步队列的形式实现了最终一致性,但是有引入了新的队列中间件。建议使用JDK自带的队列实现该异步补偿(在JVM进程内完成异步补偿)而不是用RabbitMQ这类网络中间件。

人工处理

在最后可以通过记录相关日志后,通过认为接入的方式完成补偿和数据同步。

感谢

如果您对我的文章感兴趣,可以关注我的个人微信公众号哦,将会第一时间同步最新的技术博客~~
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值