Redis(四)- 分布式锁
目录
基于Redis的分布式锁优化【开源框架,前面的都是经典白学】
1.Redission实现可重入锁底层原理【解决setnx下不可重入的问题】
2.Redission实现获取锁失败时的重试机制:【解决setnx下不可重试与超时释放的问题】
3.Redission的multiLock分布式锁解决setnx下主从一致性问题
一人一单问题引出分布式锁
对于一些优惠力度较大的优惠券 我们只允许一人购买一单。进行实现这个需求:

演示流程:
1.

2.封装核心方法
@Transactional
public Result createVoucherOrder(Long voucherId) {
//5.实现一人一单
Long userId = UserHolder.getUser().getId();
//5.1 查询订单
int count = query().eq("user_id",userId).eq("voucher_id", voucherId).count() ;
//5.2 判断是否存在
if (count > 0) {
//说明用户已经购买过了
return Result.fail("用户已经购买过一次");
}
//6.扣减库存
//update 表名 set stock = stock -1 where voucher_id = voucherId and stock>0
//使用的是stock>0作为乐观锁的条件
boolean success = seckillVoucherService.update().setSql("stock = stock -1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update() ;
if (!success) {
//扣减失败
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder() ;
//7.1订单id
long orderId = redisIdWorker.nextId("order") ;
voucherOrder.setId(orderId) ;
//7.2 用户id
voucherOrder.setUserId(userId) ;
//7.3 代金券id
voucherOrder.setVoucherId(voucherId) ;
save(voucherOrder) ;
//8.返回订单id
return Result.ok(orderId);
}
3.使用悲观锁进行上锁调用
把锁加到整个方法外面,并且确保每一个对象过来时 ,每一个用户id对应锁的唯一性。
并且记住在Spring管理中,只有代理对象才可以进行开启事务

4.

集群模式下的线程并发安全问题
如图所示:进行设置集群。
当我们进行拷贝一份启动类之后 就相当于是让SpringBoot底层为我们进行多搞出了一个tomcat服务器 !这就是集群操作。


一人一单的并发安全问题:
在集群模式下或叫做是分布式模式下,外面都会存在一人一单并发安全问题。因为在这种情况下,有多个Tomcat服务器。
一个Tomcat服务器一个新的JVM,会是一个新的锁监视器对象。因此会出现上锁之后 依然会并发执行的情况。

eg:
对于同一个userId,锁对象只有一个。对于同一个JVM可以完美的限制并发线程问题 从而实现一人一单。但是对于分布式或集群情况下,
锁监视器不同,那么就会导致线程并发问题出现。

解决方法见下一个目录
分布式锁
实现跨JVM的锁的方案。

什么是分布式锁?


基于Redis的分布式锁
服务器宕机是指网络 游戏和一些网络应用的服务器非正常运行的一种状态,会导致锁无法进行获取也无法进行释放,然后就会导致死锁

最终:setnx表示的意思就是:我们进行setnx的时候,进行setnx一个key,我们必须要保证对于这个key值在Redis中没有进行设置过任
何值,这样才可以setnx成功。否则失败 !

使用分布式锁进行解决集群登录情况下并发问题:

步骤演示:
1.

2.

3.

4.测试
使用分布式锁 解决集群模式下线程的并发安全问题:
这个问题就是:集群模式下不同tomcat服务器 对应不同的JVM 对应不同的锁监视器。
所以我们使用分布式锁进行解决这个问题。让它们被同一个锁监视器进行管理 !

出现极端情况时:
线程1由于业务阻塞时间过久,最终导致超时释放锁。
线程2借此机会进入获取锁 然后执行业务
当线程1执行完业务时 它会进行释放锁 但是此时释放的锁是线程2的锁
此时线程3进入获取到线程1释放的锁 会与线程2并发执行 !!
这样就不符合我们的预期效果啦,就出现问题了 !!!

解决方案:
这个题目的错误之处就在于线程1释放了不属于它的锁从而导致的问题错误 !
所以说我们在释放一个锁之前 要进行判断这个锁是否是自己的锁 然后再去决定是否释放 !


代码演示:


出现极端情况时:

我们的目的就是需要保证多条命令执行时的原子性。会使用到一种脚本,即是后面所学的Lua脚本。
出现极端情况时 我们进行释放锁:
释放锁的过程中我们可能会导致误删,因为这具有多行代码,在阻塞的过程中可能会导致误删

解决方法:使用Lua脚本进行改造
Lua脚本官方网站
Lua脚本如何调用Redis?



eg:


进行简化脚本代码:使用KEYS和ARGV两个数组进行接收key和value

使用Java代码进行集成Lua脚本语言代码
在我们的Redis底层已经提供了关于Lua脚本相关的API方法。可以直接进行调用然后执行。

步骤演示:
1.编写Lua脚本

2.调用这个脚本 保证了并发过程中事务的原子性
http://t.csdn.cn/GeMqk(原子性解释)

因此我们把释放锁的代码由多行进行简化为一行,确保了多行代码执行 具有原子性,避免误删的情况发生。

到目前为止,基于Redis的分布式锁已经相对比较的完善了:

基于Redis的分布式锁优化【开源框架,前面的都是经典白学】
总结基于setnx实现的分布式锁存在以下的问题:
我们的Redission框架就是进行解决这四个问题的:
问题1:不可重入
分析:方法A获取一把锁,然后执行方法A 方法A中进行调用方法B ,但是对于方法B的执行需要获取和方法A相同的锁,此时就会造成死锁 !
问题2:不可重试
分析:进行获取锁,一次获取失败我们想要多重试获取几次
问题3:超时释放
问题4:主从一致性
主从数据同步存在延迟,如果在未完成主从数据同步的情况下,主机宕机 那么就会导致锁失效。最终导致其他线程进入访问造成并发


Redisson步骤演示:


步骤演示:
1.

2.
找到虚拟机对应的ip地址

创建配置类 在配置类中进行配置相关想要使用的Bean对象

进行注入我们配置的我们自己的RedissonClient对象:

进行调用框架相关的API:

什么是重入锁?
在同一个线程内,多次获取同一把锁(所谓同一把锁的标识就是:是在同一个线程中执行获取的)。
获取锁的次数就是重入锁的次数。

基于Redis不可以实现重入锁

1.Redission实现可重入锁底层原理【解决setnx下不可重入的问题】
分析:
我们基于Redis是不可以实现可重入锁的。因为只是单单的KEY-VALUE结构,当第二次setnx key value时 由于是相同的key 则就会执行失败。执行失败导致的结果就是重入失败。
但是基于Redisson时 ,就得到了很好的解决。我们采用的结果是Key-Value Hash结构。Value字段是由一个field-value hash结构构成的
field用于存储线程对应的线程名称。value用于锁计数数值大小。
整体流程记录:
前提须知:
tryLock()方法表示获取锁 获取一次则value+1
unlock()表示释放锁 释放一次锁则value-1
当最终value的值等于0时,就可以安全的释放锁啦 !这就是可重入锁的原理。
流程:
当我们开启一个线程进入之后,执行trylock获取锁的操作。获取锁的标识 KEY记作为lock。底层会进行确定该线程对应的线程名是什么。然后把其存入到field字段中,由于是获取锁的操作,因此锁计数+1,即是value+1。在这个操作的过程中需要一定的时间,所以我们需要重新设置锁的有效期。
如果在同一个线程中又再一次的执行trylock方法 即是获取锁的方法时,底层会看出KEY是一致的,都为lock 表示意思为想要获取的是同一把锁。但是我们还需要进行检验field字段是否相同,如果是在同一个线程进行再一次获取这把KEY为lock的锁时,线程名肯定相同,那么锁计数+1 即是value+1。
如果不是在同一个线程中进行获取这把KEY为lock的锁时,那么field不相同,因此当我们重入时,即是重复进行获取锁时,会获取锁失败。那么直接进行退出了。
在这个操作的过程中需要一定的时间,所以我们需要重新设置锁的有效期。
当我们执行完一段业务之后,进行释放锁的操作 执行unlock方法。先进行判断锁是否是自己的这把锁:如果是,value-1。如果不是,直接出去 并且表示锁已释放。
然后进行判断value是否为0,如果为0,那么释放锁成功。如果value不为0,先进行重置锁的有效期,说明此时还没有执行返回到最外层。然后继续执行业务,继续执行unlock方法,直到有一次发现value==0了,那么此时释放锁 !

具体实现步骤:
由于Java代码实现过于复杂,我们采用的方法是 Java代码集成Luna脚本进行实现:
1.Luna 脚本实现获取锁的流程:

2.Luna脚本实现释放锁的流程:

代码:
--1. 获取锁的Lua脚本
--锁的KEY
local key = KEYS[1] ;
--线程的唯一标识
local threadId = ARGV[1] ;
--锁的自动释放时间
local releaseTime = ARGV[2] ;
--判断锁是否存在
if (redis.call('exists',key)==0) then
--返回0 说明锁不存在,可以进行获取锁
--'1'标示着有线程第一次进入获取这个锁
redis.call('hset',key,threadId,'1') ;
redis.call('expire',key,releaseTime) ;--设置有效时间
return 1 ;--返回1 在Lua中 返回值只有1或0
end ;
--锁已经存在 ,判断threadId是否是自己
if (redis.call('HEXISTS',key,threadId)==1) then
--获取锁 重入次数+1 1 表示设置的递增步长为1
redis.call('HINCRBY',key,threadId,1)
redis.call('expire',key,releaseTime) ;--设置有效时间
return 1 ;
end
return 0 ;--代码走到这里说明获取锁失败 ,key在自己之前已经存在过了 !
--2. 释放锁的Lua脚本
local key = KEYS[1] ;--锁的Key
local threadId = ARGV[1] ;--线程的唯一标识
local releaseTime = ARGV[2] ;--锁的自动释放时间
--判断当前锁是否为自己持有
if (redis.call('HEXISTS',key,threadId)==0) then
return nil ;
end
--是自己持有的锁 那么把重入次数减 1 -1表示设置的递减步长为-1
local count = redis.call ('HINCRBY',key,threadId,-1) ;
--判断是否重入次数已经为0
if (count>0) then
--大于0 说明不能释放锁,重置有效期然后返回
redis.call('expire',key,releaseTime) ;
return nil ;
else --要是count等于0说明可以释放锁 直接删除
redis.call('del',key) ;
return nil ;
end
总结基于setnx实现的分布式锁存在以下的问题:

2.Redission实现获取锁失败时的重试机制:【解决setnx下不可重试与超时释放的问题】
这种机制,在一定程度上提升了获取锁失败时,去重新获取锁的效率。
注意点:
1.当ttl为null 时,表示获取锁成功。不为null时,表示获取锁失败。

总结:

3.Redission的multiLock分布式锁解决setnx下主从一致性问题
3.Redisson的multiLock:
服务宕机而导致锁失效的问题是什么:
多个服务器 ,最终形成主从同步。当一个主机服务器宕机之后,就会导致锁失效 !因为只有这个宕机的主机进行获取了锁(set lock thread1 NX EX 10)。其余子机并没有进行获取锁。最终由于主机宕机之后,就算是一个子机自动进行演变为主机代替宕机的服务器主机进行工作 但是锁已经失效了 !!!

为解决这个问题:
我们使用的是Redisson的multiLock:
原理为:
我们使用的是联锁机制。开启三个Redis Node节点,三个Redis Node对应三把锁,这三把锁联合在一起组成联锁。这三个Redis Node对
应着三个Redis Slave 从服务器,我们进行主从一致数据同步。
假设此时有一个Redis Node节点宕机失效了,那么这个Redis Node对应的锁也失效了,此时其他的线程也可以进行获取这个宕机的Redis Node节点对应的从服务器的锁。
但是由于是联锁的原因,我们在并没有获取全部的三把锁时 就相当于没有获得锁。没有获取锁 那么就不会造成破坏
缺陷:
运维成本高,实现复杂


分布式锁的最终总结[分布式锁的三步优化]:
1.不可重入Redis分布式锁:
原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
缺陷:不可重入,无法重试,锁超时失效。
不可重入:不可以在同一个线程内多次获取锁
无法重试:
锁超时失效:当达到超时时长之后,为了避免死锁发生 会直接让锁失效
2.可重入的Redis分布式锁:
原理:
利用hash结构,记录线程标示和重入次数:我们把锁标示存入KEY对应位置,把开启的线程对应的线程名存入到VALUE对应的hash结构中的field字段,对于锁计量重入次数存入到field-value中的value中。然后进行一系列操作,最终实现在同一个线程中可实现多次重入。【具体见上面的分析】
利用watchDog延续锁时间:不会因为锁超时而失效,只会因为服务宕机而导致锁失效。
利用信号量控制锁重试等待:意思就是当获取锁失败之后,我们进行重试,重试意思就是重新进行去获取锁。利用的是发布订阅的方案,
这种操作对cpu的利用率比较高 不会有过多的无效等待。
3.Redisson的multiLock:
服务宕机而导致锁失效的问题是什么:
多个服务器 ,最终形成主从同步。当一个主机服务器宕机之后,就会导致锁失效 !因为只有这个宕机的主机进行获取了锁(set lock thread1 NX EX 10)。其余子机并没有进行获取锁。最终由于主机宕机之后,就算是一个子机自动进行演变为主机代替宕机的服务器主机进行工作 但是锁已经失效了 !!!

为解决这个问题:
我们使用的是Redisson的multiLock:
原理为:
我们使用的是联锁机制。开启三个Redis Node节点,三个Redis Node对应三把锁,这三把锁联合在一起组成联锁。这三个Redis Node对
应着三个Redis Slave 从服务器,我们进行主从一致数据同步。
假设此时有一个Redis Node节点宕机失效了,那么这个Redis Node对应的锁也失效了并且原来对应的从服务器变成主服务器了,此时其他的线程也可以进行获取这个宕机的Redis Node节点对应的从服务器的锁。
但是由于是联锁的原因,我们在并没有获取全部的三把锁时 就相当于没有获得锁。没有获取锁 那么就不会造成破坏
缺陷:
运维成本高,实现复杂


更多推荐
所有评论(0)