Bootstrap

Redis一致性与分布式锁

Redis一致性

何为redis一致性

即在项目中,redis缓存中的数据要与数据库当中的数据保持一致。

那么这里,就会有小伙伴要问了,redis缓存中的数据不就是从数据库当中查询出来的吗?怎么会不一致呢?

笔者在这里解答一下这个问题嘿嘿,在简单项目中,redis一致性确实不用太过考虑,可是在高并发的项目中,就必须考虑redis的一致性了,举个很简单的例子,比如某平台发起了一个1元抢iphone15的活动,总共只有一台,但却又数万人守在电脑面前跃跃欲试。我们来设想一下,假如成功抢到的那个用户,点击抢购按钮后,是不是redis缓存中的手机数量就给删除了,就没得抢了嘛,然后后台数据库也要进行更新,将可以秒杀的手机数量变为0是不是?

但我们在现实开发过程中,必须要考虑一个很现实的问题:网络波动

我们假设在第一个用户点下抢购按钮,也的确将redis缓存中的秒杀手机这一数据给删除了,但是在将redis的数据更新到数据库这一过程中,产生了网络波动,导致更新操作一直未完成。就在这个网络波动的时间段内,又有一个用户点击了抢购按钮,此时他的请求发到服务器后,服务器发现redis缓存没有秒杀手机数量的数据,于是便会去查询数据库,但此时数据库中的数据并未更新,可供秒杀的手机数量仍然为1,便将这个数据返回给redis,并且这个用户也会显示抢购成功。虽然后面网络波动结束,数据库成功更新了,不会再有任何用户秒杀到手机,但明明只有一台的名额,却有两个用户秒杀到了,这个明显是有问题的,这也就是redis一致性要解决的。

为什么会有redis一致性

在微服务分布式项目中,可能存在数据库更新了,但redis未更新,从而使得客户端所得到的数据(客户端数据从redis缓存当中获取),与后台实际数据库当中的数据不一致的问题。即当高并发的情况下,存在多个用户同时发起一个请求,都需要查询redis中的数据。

上述也有举例,大家可以结合起来理解。

双删一致性

现在我们再假设一个业务场景:

图灵解读:现在有两个线程,分别为线程1和线程2,首先线程1对redis缓存中的数据进行删除,并对数据库进行对应的更新,但由于网络延迟,数据库并未及时更新;与此同时,在网络延迟的时间中,线程2想从redis中查询数据,但由于线程1删除了redis缓存中的数据,所以便会向数据库直接查询,但由于网络延迟,线程1发起的数据库更新操作还未实现,故此时线程2通过redis从数据库中查询到的数据为旧数据,并此旧数据还会存储在redis中,当线程1更新数据库的操作完成之后,此时redis缓存中的数据便与数据库中的数据完全不一致了,并当后续更多线程来查询数据时,也都是会从redis中查询到旧数据

解决方案(双删一致性流程):

即在上述的业务的场景中,我们在线程一对数据的更新操作完成了之后,将当前redis缓存中的数据进行一个删除,这样后续的查询请求便会直接查询数据库当中的最新数据,并将最新的数据存入redis缓存中;因为一共删除了两次数据,即线程1删除了一次,并且线程1对数据库的更新操作执行完成之后,又删除了一次redis,故叫做双删一致性

双删一致性注意点:

在第二次删除的时候,我们需要延迟几百毫秒再进行删除。原因:避免因为线程2将数据库中的老数据放入到redis这一过程产生延迟,导致线程1的第二次删除redis缓存这一操作发生在线程2将老数据放入redis之前,导致redis中仍旧保存的是数据库中到的老数据

双写一致性

流程:

先操作数据库,再删除redis;即数据库中的数据发生了更新后,redis缓存中的数据也要进行一致的更新

如何实现:

方案:如果对实际业务的影响不大,则不管;如果对实际业务有较大影响,则可通过双删来处理

分布式锁

定义

单体项目实现分布式锁:在单进程(启动一个jvm)的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实现的;即多个线程共用一把锁,锁只能被一个线程使用,其余线程只能排队等待使用。

基于数据库操作

乐观锁

定义

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做

如何使用

表中添加一个时间戳或者版本号的字段来实现,update account set version = version + 1 where id = #{id} and version = #{oldVersion} 当更新不成功,客户端重试,重新读取最新的版本号或时间戳,再次尝试更新,类似 CAS 机制,推荐使用。

悲观锁

定义

假设在并发的情况下,数据处理很容易发生冲突。为了保证数据的一致性和完整性,故每次在读写数据的时候都会先加锁

如何使用

适合于写比较多的操作,先加上锁保证数据的完整和一致性,其余进程等待即可,通过悲观锁锁上,业务需求的强一致性较高,则可以通过悲观锁先锁上,然后一个进程一个进程的进行数据的操作

基于redis缓存与过期时间

redisson执行流程

图文解读

多个线程竞争一个锁,最终只能有一个线程获取到这个锁。通过uuid与线程id作为加锁的key,保证当前锁与当前线程的正确绑定,在线程进行业务处理的时候,内部会有一个watch dog,也就是常说的看门狗机制,它会每隔10s去看一下该线程是否还持有锁,如果还持有的话,则延迟生存时间,给锁进行续命,如果redis进行了集群的话,还会通过lua脚本来进行redis的选择;如果没有获取锁,则会进行自旋,直到成功获取锁 

面试重点题

当redis使用集群时,如果主节点master挂了,如何保持数据的一致性?

答:通过redisson提供的red lock来进行解决,red lock功能:针对redis中的所有节点进行同步,所有节点同步成功后,才会返回锁存储成功,从而达成强一致性。

基于zookeeper临时节点与watch

公平锁

非公平锁

根据Zookeeper的临时节点的特性实现分布式锁,先执行的线程在zookeeper创建一个临时节点,代表获取到锁,后执行的线程需要等待,直到临时节点被删除说明锁被释放,第二个线程可以尝试获取锁。

写在最后:

本篇文章给大家介绍了些redis一致性的知识点,希望能够给大家带来帮助。笔者小,中,大厂均有面试经验,目前正在从事全栈开发工作,坚持每日分享java全栈开发知识与相关的面试真题,希望能够给大家带来帮助,同大家共同进步。

;