分布式锁的实现

分布式锁

分布式锁的引出

单体锁存在的问题:共享数据不安全,超卖现象

  在单体应用中,如果我们对共享数据不进行加锁操作,多线程操作共享数据时会出现共享数据不安全问题。先可以看下面的例子,也可以来看看买票。假设我们使用对共享数据不进行加锁操作,会出现一张票卖给了多个人,这就发生了一个抢票过程中,我们都知道的名词,也就是我们所说的超卖现象
 
在这里插入图片描述

 

  我们的解决办法通常是加锁。如加单体锁(synchronized或RentranLock)来保证单个实例并发安全。

这里拓展一波:

锁的理解

我们为什么需要用到锁?我们在多个线程并发过程中需要用到锁。
用到锁的原因是有一个共享资源,多个线程都需要去修改它或者是扣减库存,那么是谁来修改或者扣减我们用锁来控制。
加了锁的目的是我访问的时候,你不能访问,你访问的时候,我不能访问,是一个互斥现象。

我们用锁分为了三个过程。
以synchronized代码块为例
1、竞争锁,谁先进入synchronized代码块里面,谁就抢到了这把锁。
2、占有锁的过程,在代码块里面,我去修改值,我去看库存,我去扣减库存过程是一个占用锁的过程。
3、释放锁,在我们执行完synchronized代码块之后退出,完成任务之后,还需要一个释放锁的过程
 

在这里插入图片描述
 

上面举例的并发量只是一点点,在这里我们延伸一下。比如我们刚过去的双12或者双11,
举一个例子,在京东中,有一台手机,原价1万块,现在搞特价变成了1000块,可能只有两台,但是在同时全国可能有几百万的人都在抢。

如果在这种情况下面,我们还是使用synchronized关键字能够解决嘛?首先在我们的高并发中,一台Tomcat能不能响应我们几百万人的请求?这肯定是不行的,其实就像我们的人一样,比如假设我们人同时只能处理10件事,但是你同时丢给他100件事,两百件事,它处理不了,它还可能闹脾气,不干了。而这讲到我们的Tomcat上面,我们的Tomcat优化得再好,它请求的并发量一定是有限的,一台Tomcat,我们的机器再牛逼,也不可能处理几百万的并发量。所以就延伸出来我们分布式的要求,集群环境。一台Tomcat处理不了,我们就搞多个Tomcat实例来处理,用多个Tomcat来处理实例,那么就自然而然出现了另外的概念就是,我们如何进行分流

拓展——分流

如果对这部分比较熟悉可以跳过。
什么是分流,假设我们有一个请求req,而我们有多台Tomcat,那么这个时候我们是分给Tomcat1,还是分给Tomcat2?
而关于分流,我们经常使用到我们的Nginx

拓展——分流Nginx简单理解

当一个请求过来了,Nginx根据它的规则,哪台服务器较闲,就把这个请求分给那台较闲的服务器。防止一个忙的时候,另一个人在旁边吃雪糕,看你干活。这就是nginx做的事情。

这个时候,我们可以处理得过来了,那么一个synchronized能不能去处理我们并发的安全问题?
我们一测试,还是发现多个服务在数据层面出现了一票多卖的超卖现象

一个tomocat实例是一个JVM进程,单体锁(synchronized、ReentrantLock)是JVM层面的锁,只能控制单个实例上的并发访问安全,多实例下依然存在一票多卖的超卖现象。

 

在这里插入图片描述
 

分布式锁的引出

由于单体锁是基于 JVM 层面上的锁,只能控制单个实例上的并发访问安全,多个实例下依然存在一票多卖的超卖问题,这个时候,就轮到我们的分布式锁出场了。
分布式锁是指:所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功获取锁,其他没有获取锁的线程必须全部等待,直到持有锁的线程释放锁。分布式锁可以跨越多个JVM,跨越多个进程的锁。

 

分布式锁的设计思路

 
由于Tomcat是由Java启动的,所以每个Tomcat可以看成一个JVM,JVM内部的锁是无法跨越多个进程的。所以,我们要实现分布式锁,我们只能在这些JVM之外去寻找,通过其他的组件来实现分布式锁。系统的架构如图所示:

两个Tomcat通过第三方的组件实现跨JVM、跨进程的分布式锁。这就是分布式锁的解决思路,找到所有JVM可以共同访问的第三方组件,通过第三方组件实现分布式锁。

在这里插入图片描述
 

分布式锁的常见应用场景

 
  一般电商网站都会遇到秒杀、特价之类的活动,大促活动有一个共同特点就是访问量激增,在高并发下会出现成千上万人抢购一个商品的场景。虽然在系统设计时会通过限流、异步、排队等方式优化,但整体的并发还是平时的数倍以上,参加活动的商品一般都是限量库存,如何防止库存超卖,避免并发问题呢?分布式锁就是一个解决方案。
 

  我们都知道,在业务开发中,为了保证在多线程下处理共享数据的安全性,需要保证同一时刻只有一个线程能处理共享数据。
 

  Java 语言给我们提供了线程锁,开放了处理锁机制的 API,比如 Synchronized、Lock 等。当一个锁被某个线程持有的时候,另一个线程尝试去获取这个锁会失败或者阻塞,直到持有锁的线程释放了该锁。在单台服务器内部,可以通过线程加锁的方式来同步,避免并发问题,那么在分布式场景下呢?
 
在这里插入图片描述
 

分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。分布式场景下解决并发问题,需要应用分布式锁技术。

文章部分内容部分引自:Redis实现分布式锁

 
 

分布式锁方案

实现分布式锁目前有三种流行方案,即基于数据库、Redis、ZooKeeper 的方案。
 
在这里插入图片描述
 

 

数据库的分布式锁如何实现

当我们想使用基于数据库的分布式锁
我们不使用JVM层面的单体锁,我们首先先要创建一张表,而这张表需要一个唯一索引,可以是主键索引或者非主键索引,但是必须保证唯一。
然后我们代码插入insert into ----- xxx(需要包括唯一索引),这个时候谁插入成功了,那么就能接着代码往下走。
如果没有插入成功,那么只能排队等待。
所以基于数据库分布式锁是通过数据库唯一约束条件来进行完成的。基于数据库的锁是比较容易实现的,我们重点来看一下使用Redis缓存数据库来实现分布式锁

 
 

Redis分布式锁如何实现

使用Reds来实现分布式锁的核心思路:
在Redis中有一个命令叫setnx。如果我们的数据库中我们setnx test 1插入数据是可以插入成功的,但是**只要我们插入成功之后我们第二次插入test,那么就会失败。**只要我们使用setnx在我们的数据库中设置了key,那么后面都设置不成功。之后线程操作完之后再delete这个key,我们就是通过setnx来延伸出Redis分布式锁的。

 
但是我们仅仅使用Redis的setnx来设计我们的分布式锁,会出现死锁问题。

我们使用我们Java代码中操作Redis的setnx操作来实现分布式锁,这样成功解决了我们的超卖问题!但是,这样的代码,不够健壮!比如我们一个线程使用setnx进行加锁操作,然后进入了代码块想往下走,这个时候突然间,我们的机器死机了,那么我们下面的代码就不会去走了,并且delete删除锁的操作也不会进行,那么其他线程就只能在外面永远的等待下去,这样就造成了死锁的概念。死锁具体看我们另一篇博客:MySQL高级理解

而我们绝对不会让它一直等待下去,就像我们在网上进行支付的时候,我们会设置一个倒计时支付时间,也就是我们对这个key设置一个超时时间。expire test 10 查看剩余时间ttl test.

设置了这个过期时间,我们就可以解决这个死锁的问题了。

但是,Redis分布式锁现在没有人用setnx,原因在于,如果我们使用setnx,我们在代码中,我们需要写两行代码,如果我们设置完了key,我们在设置超时时间的同时,这个时候宕机了,同样会出现死锁的问题。setnx现在没有人使用是因为它不满足原子性

所以我们使用set key value EX time NX来解决,原子性的加锁加过期时间
 
在这里插入图片描述

 

但是,在这个过程中,还会出现另外一种问题。

假设我们有3个请求,请求1、请求2、请求3.
我们在线程加锁的时候,同时会设置一个过期时间来防止死锁。而我们在执行业务的时候,会有一个执行业务的时间。所以另外一种问题就是,我们的锁的过期时间 < 业务执行时间。
比如我们请求1抢到锁之后,加锁的时间为5s钟,但是由于今天我们的网络不好或者是机器性能下降,我们的业务执行了10s钟的时间。我们在到达5s钟的时候,锁的过期时间就到了,这个时候,请求2也就可以抢到锁并设置了,这个时候,请求2和请求1就同时进行,违反了锁的互斥条件

还有一种情况同样是锁的过期时间 < 业务执行时间,而请求1正在执行,请求2也在执行,但是在这个过程中,请求1完成了,所以请求1会执行删除锁的操作,这个时候请求3依然可以抢占锁。而这个过程中,真正持有锁的是请求2,而请求1把请求2的锁给删除了,导致请求3也来抢占锁,这个时候,出现了第二个问题,就是锁的误删。(解决需要加一个唯一的标识)

出现这个原因在于,没有验证这个锁是不是自己加的,所以我们要验证一下,在进行删除之前,我们要验证一下锁是不是我加的,如果是我加的就删,如果不是就不管它。这个时候,我们可以设置我们Value的值,来确定是哪一个人来加的锁,也就是判断value中的值。

 

在这里插入图片描述 
删除锁的时候要进行判断

 
在这里插入图片描述

在这里插入图片描述 

这个过程也不是一个原子性操作,而Redis也并没有删除的扩展命令,而我们在分布式环境下去删除某些东西的时候,需要用到原子性操作的时候,一定呀结合lua脚本实现。
 

在这里插入图片描述
 
在这里插入图片描述
 

Redis的分布式锁的租约问题

而我们回过头来看,解决互斥被破坏的情况。
出现互斥被破坏,出现锁的误删的原因在于我们的业务执行时间 > 锁的超时时间,我们的锁过期了,我们依旧在执行业务,这个时候我们能不能实现,如果我们的业务没有完成之前,我们不要去删除这把锁呢?
其实这就是一个Redis分布式锁的租约问题,具体租约问题可以看我另一篇问题:Redis分布式锁的租约
解决这个问题需要用锁的续期,而这个锁的续期,我们使用守护线程来实现。
 

在这里插入图片描述
 
如果我们像淘宝、京东那些有几百个Tomcat,那么我们一个redis节点,能100%顶住么?答案肯定是不可用的,所以我们想使用我们Redis集群来实现我们的分布式锁的问题。

我们解决了第一个,第二个,看第三个,我们就可以来说我们的Redis集群了。Redis集群具体看我的另一篇博客:Redis缓存数据库

但是我们的三种Redis集群也不能解决高可用的问题。
我们一个一个来看
主从模式:我们主从模式,主节点负责我们写部分,而我们从节点是负责读,而我们那么多的读请求都到我们的主节点上面。而我们使用Redis分布式锁,我们用的命令是set命令,是写的命令,所以根据我们的主从架构,我们只能走左边这条线,而没有分担一点压力的目的,而我们的主节点挂掉之后,我们的从节点依然是接收不了服务的,所以没有实现高可用,主从模式在我们Redis实现分布式锁是不行的。

 
在这里插入图片描述
 

哨兵模式:哨兵对主从模式中的每个节点进行监控,当 master 节点出现故障时通知投票机制,选择新的 master 节点,实现了故障转移。那这样子set命令还是有人执行的,为什么又不可以呢?

但是我们要明白一个问题:Master写完的数据一定要同步到Slave节点,而主从数据之间的同步是直接写Master,Master如果写成功了那就直接返回,从的不关心,从的定期从Master去同步就可以了,我们回过头来看,如果我们Redis实现分布式锁的过程中,我们主节点进行加锁set操作,而这个时候从节点还没有同步master里面的数据,这个时候master节点挂掉了,丢失了master中的数据,根据哨兵,此时选举一个slave变成master,而这个时候新的master可以响应加锁请求。而这个情况对于我们的业务来说,同时有两个请求加锁成功,还是违反了互斥特点,出现超卖现象,所以哨兵也不行。

 

在这里插入图片描述
 
而还有一种情况就是,如果此时,我们的并发量很高,一瞬间有数以万计的请求过来,我们都需要进行加锁,远远超过了我们的单节点能处理的情况,所以我们的master响应加锁的(写)请求很容易就挂掉了,而哨兵模式中slave也不能分担master节点加锁(写)的压力,而哨兵选举新的master之后,也可能因为请求冲击挂掉,以此进行出现连环雪崩现象。哨兵情况下不能达到一个分担压力的目的

集群cluter模式:

集群模式最基本的来说就是三主三从。
集群模式中数据通过数据分片的方式被自动分割到不同的master节点上,每个Redis集群有16384个哈希槽,进行set操作时,每个key会通过CRC16校验后再对16384取模来决定放置在哪个槽。

而我们使用Redis去实现分布式锁的时候,我们使用的是set test 1 ex 10 nx,而我们这个test这个key,有且只可能落在某一个节点上面。而如果我们此时用了cluster模式,我们一台节点的请求并发量为10w,而我们来了20w的并发加锁(写)请求,那么又是一个master在干活,另外两个节点在空闲着。又是哨兵模式的那两个问题
 

在这里插入图片描述
 
在这里插入图片描述
 
所以说,Redis高可用集群的三种模式都不能作为Redis集群下实现分布式锁的高可用方式。

那么有什么方法可以解决?
第一个方案:
首先来解决数据丢失导致的同时有两个请求加锁的情况。怎么会触发数据丢失的问题,原因在于新的master选举出来之后,里面并没有老的master节点中部分数据,导致了同时有两个请求加锁的情况。而我们在Redsi加锁的请求中我们会设置一个expire过期时间,所以解决方法就来了,我们牺牲掉一定的服务可用时间,我们在我锁的过期时间之后,我们再去启动新的master的上位(延迟启动)。当我们的锁超过超时时间之后,我们再去启动新的master节点,这个时候,即使我们master节点中有一部分数据没有同步过来,这个时候没有问题,因为过了expire超时时间,这部分数据一定会失去的,而我们延迟启动,就自然而然,解决了数据丢失而导致的同时有多个请求加锁的情况。这个延迟启动在Redis哨兵模式中的参数中可以配置。

第二个方案:
Redis高可用方案,一般我使用RedLock算法来 解决redis分布式锁的高可用问题,原因可以从我们上述Redis分布式锁的高可用方案分析可知道,RedLock算法是就是为了解决我们Redis主从之间同步数据使用异步的方式造成锁数据丢失而无法利用多个Redis节点达到高可用的问题。

我们平常使用jedis
但是还有一个redission,在redission里面就提供了Redlock算法的 分布式锁,具体实现很简单。
 
在这里插入图片描述
 
RedLock要求:
1、用多台服务器来确保redis分布式锁的高可用状态,但是上面这五个redis节点之间是相互独立的,之间没有任何的主从哨兵cluster关系,只是我们在电脑1/2、3/4、5中安装了redis服务的关系。
2、我们锁的过期时间,一定要远远大于我们的加锁时间。
3、如果我们加锁失败,有三个客户端,1/2、3. 客户端1弄redis1和redis2。客户端2弄redis3和redis4,客户端3弄redis5。而RedLock算法而已,加锁要过半数才觉得分布式锁加锁成功,而此时的情况是221,没过半。所以会不停地重试,所以要弄重试次数。

RedLock算法:
第一步:加锁,加锁的时候,首先会获取一个时间戳,拿到这个时间戳之后,我们的Client会按顺序用key value去访问我们redis的服务器来看看锁有没有加载成功,假设1/2、3插入成功之后,超过半数了,此时,认为分布式锁加载成功了。

第二步:释放锁,只要向所有的redis实例发送delete命令,不用关系关系里面到底有没有释放成功即可,有就删除,没有就不管了。

这就是redlock算法的基本使用。
理论挺麻烦,实现很简单。

而RedLock底层是使用Lua脚本来实现的。
 
在这里插入图片描述

 
 

Zookeeper分布式锁如何实现

我们Zookeeper实现分布式锁的方式有两种,一种是使用Java原生API来实现,一种是使用Curator 框架来实现。我们使用Java原生API,可以得知Zookeeper实现分布式锁的大致流程,具体Zookeeper实现分布式锁见我另一篇博客:Zookeeper

 
  
  
  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值