当前位置: 首页 > article >正文

彻底理解如何保证Redis和数据库数据一致性问题

一.背景

系统中缓存最常用的策略是:服务端需要同时维护 DB 和 Cache 并且是以 DB 的结果为准,那么就可能出现 DB 和 Cache 数据不一致的问题。

二.读数据

逻辑如下:

当客户端发起查询数据的请求,首先回去Redis中查看没有没该数据,有就返回,没有的话就需要去数据库中查询该数据,并且将该数据缓存在Redis中,然后设置一个过期时间(过期时间的作用就是为了保证一些冷数据能及时被清理,不长时间保存在Redis中,占用Redis的空间),保证在Redis后,返回数据到客户端,这是最常见的一个数据读取场景。

如果我们只读的话,肯定不会出现数据不一致问题,一般是读和写并发的时候才会出现数据不一致问题

三.写数据

分如下两种方式:

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

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

1.删除缓存还是更新缓存?

答:删除!这是因为更新操作相对来说更为复杂,涉及到数据的一致性和同步性等问题;而删除操作相对简单,只需要将缓存中的对应键值对删除即可;在实际的缓存场景中,我们经常会遇到数据更新的频繁性和数据量的大规模,如果每次更新操作都需要同步更新缓存,那么会增加系统的复杂性和开销,而采用只删不更新的方式,可以避免这些问题,当数据被修改时,我们只需要删除缓存中的对应键值对,下一次访问时再重新从数据库或其他数据源中获取最新的数据,并将其存储到缓存中,这种方式可以减少数据同步的开销,并且保证缓存数据的一致性。

2.先操作数据库还是缓存?

(1)先操作缓存

写的场景:先把Redis的数据删除,然后再写入数据库

读写并发场景,如何导致数据不一致:

1)线程1发起修改操作,然后删除Redis中的缓存,此时网络出现卡顿;

2)线程2由于是并发进来,他执行的是一个查询的请求,他会去Redis中查数据,查不到数据,所以会去数据库中查询,此时它查询的是老数据,并且会把数据缓存到Redis中;

3)线程1网络卡顿结束,并把最新数据更新到数据库中;

4)此时数据库是新数据,Redis是老数据,后面再有线程查询请求,请求到的都是老数据,必须等到老数据的过期时间到了,才能重新查询数据库获取到新数据,这个过程之间,一直都会数据不一致。 

解决方案:

线程1把数据库修改成新的后,再执行一遍删除Redis缓存操作,但是线程2查询到的肯定是老数据,所以只会出现数据的一次不一致

那能不能保证不一致一次都不出现?

这就需要引入强一致性概念,如果数据库和Redis是强一致性的话,就必须要保证他们的操作是原子性,因为我们的Redis和数据库是在不同服务器上的,如果要保证他们的操作原子性,就需要去上一把锁,但是加锁后会影响系统的性能,然而我们用Redis就是为了提高系统性能,此时又为了保证强一致性,又去加锁,这样就得不偿失了,所以说,强一致性和性能我们只能保证一种,AP和CP只能保证一种,我们在AP的基础上,就可以采用双删方式保证数据的一致性,虽然会出现一次数据不一致,但是Redis和数据库他们最终的数据都是最新的,所以我们要保存数据一致性的时候会采用这种最终一致性

双删的时候为什么要采用延迟删除?

如果不进行延迟删除,在线程1删除数据后,此时线程2才把老数据放入Redis中,此时的删除相当于没删,同样会出现数据不一致问题,所以需要采用延迟双删方式

延时双删过程:

1)先删除缓存;

2)写数据库;

3)休眠500毫秒,在删除缓存;

这样子最多其他线程在这500毫秒获取到脏数据,这是无法避免的,这个500毫秒不是固定的,需要结合项目的具体业务进行判定(线程2查数据库到放数据到Redis的时间来判定)。

(2)先操作数据库

 写的场景:先写入数据库,然后再把Redis的数据删除

读写并发场景,如何导致数据不一致:

1)线程1发起修改操作,写入数据到数据库,数据库为最新数据;

2)线程2同时并发进来,进行一个查询操作,查询到时老数据,直接返回;

3)线程1然后把Redis的老数据删除,其他线程在进来,查询Redis没有,就会直接查询数据库,并缓存最新数据到Redis。

这样操作下来,也能保证数据的最终一致性,所以推荐先操作数据库,再删除缓存,只是在操作数据库时,其他线程在这个过程中查询的是脏数据。

但是这个操作是否还有问题?

有!比如说数据库插入了最新数据后,删除缓存失败,后续线程进来查询都是老数据,也是需要等待Redis缓存时间过期才能查询到最新的,虽然这是比较极端的情况,但是还是有可能出现,针对删除失败的情况,我们可以采用删除缓存重试机制

可以在删除缓存失败后,异步发送需要删除的Key到MQ中,然后监听MQ,进行重试删除, 如果在设定的重新次数后,还是删除失败,就需要记录日志,让开发人员进行排查问题。

但是重试操作带来的后果就是加入MQ,所以逻辑都放在业务代码中,增加了代码的耦合性,那么要想解耦,可以使用另外一个组件Canal。

(3)Canal解耦

Canal是阿里的一款开源框架,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费,Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端,如下图:

我们可以利用Canal提供的Java客户端,监听Canal通知消息,当收到变化的消息时,完成对缓存的更新。

不管前面先删除缓存,还是先写数据库,最终在数据进行修改的时候,canal会从订阅到binlog日志中将变更的消息通知到canal客户端(Springboot应用),然后进行删除操作,如果删除失败,发送消息到MQ,进行重复删除,这些操作都是在canal客户端进行的,从而个根业务代码进行了解耦。


http://www.kler.cn/a/407861.html

相关文章:

  • Python的3D可视化库 - vedo (2)visual子模块 基本可视化行为
  • MD5算法的学习
  • 微软发布Win11 24H2系统11月可选更新KB5046740!
  • MySQL 中的锁
  • 分类算法——基于heart数据集实现
  • SpringCloud SaToken整合微服务 集成Redis 网关路由权限拦截 服务间内部调用鉴权
  • K8s 一键部署 MongoDB 的 Replica-Set 和 MongoDB-Express
  • 《AI大模型开发笔记》Faster-Whisper 免费开源的高性能语音识别模型
  • 国外云计算服务器租用攻略
  • QDUOJ(青岛大学在线评测系统)
  • 力扣 238. 除自身以外数组的乘积
  • muduo库的使用
  • 【数据结构】—— 双向循环链表
  • PaddleNLP的环境配置:
  • 如何在Linux系统中排查GPU上运行的程序
  • 阿里云ECS服务器监控报警配置
  • 社交电商专业赋能高校教育与产业协同发展:定制开发AI智能名片及2+1链动商城小程序的创新驱动
  • 加速科技精彩亮相中国国际半导体博览会IC China 2024
  • 解决 GitHub 克隆私有仓库时的身份验证失败问题
  • uni app下开发AI运动小程序解决方案
  • LDR6020驱动的Type-C接口显示器解决方案
  • (免费送源码)计算机毕业设计原创定制:Java+JSP+HTML+JQUERY+AJAX+MySQL springboot计算机类专业考研学习网站管理系统
  • 数字八股文
  • web——sqliabs靶场——第十二关——(基于错误的双引号 POST 型字符型变形的注入)
  • 数据结构:链表进阶
  • React Native的`react-native-reanimated`库中的`useAnimatedStyle`钩子来创建一个动画样式