黑马点评--分布式锁
创始人
2024-04-11 00:26:35

黑马点评–分布式锁

基本原理与不同实现方式对比:

什么是分布式锁:

​ 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5QtJqnw-1668929831830)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116131531634.png)]

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vHYKZwwQ-1668929831832)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116132436494.png)]

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

    • 互斥:确保只能有一个线程获取锁

    • set lock thread1 nx ex 10
      
  • 释放锁:

    • 手动释放

    • 超时释放:获取锁时添加一个超时时间

    • Del key
      

流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ihnxfa0x-1668929831832)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221116134016213.png)]

基于Redis实现分布式锁初级版本:

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

public interface ILock {/*** 尝试获取锁* @param timeoutSec 锁持有的超时时间,过期后自动释放* @return true代表获取锁成功;false代表获取锁失败*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}

实现ILock接口:

public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;//锁的名称private String name;public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}//锁的前缀private static final String KEY_PREFIX ="lock:";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程表示long threadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX+name);}
}

对秒杀劵一人一单进行分布式锁实现:

  @Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result seckillVoucher(Long voucherId) {//1.查询优惠劵SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.判断秒杀是否开始LocalDateTime beginTime = voucher.getBeginTime();if (beginTime.isAfter(LocalDateTime.now())) {//尚未开始return Result.fail("活动尚未开始");}//3.判断秒杀是否已经结束LocalDateTime endTime = voucher.getEndTime();if (LocalDateTime.now().isAfter(endTime)) {//已结束return Result.fail("活动已经结束");}//4判断库存是否充足if (voucher.getStock() < 1) {//库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();
//        synchronized (userId.toString().intern()){//创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁boolean tryLock = lock.tryLock(1200);//判断获取锁成功if (!tryLock){//获取锁失败,返回错误或重试return Result.fail("一个人允许下一单");}try {//获取spring事务代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} catch (IllegalStateException e) {e.printStackTrace();}finally {//释放锁lock.unlock();}
//    }return Result.fail("抢购失败");}@Transactionalpublic  Result createVoucherOrder(Long voucherId) {//6.一个人一单Long userId = UserHolder.getUser().getId();//6.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//6.2判断是否存在if (count > 0) {//用户以及购买过return Result.fail("用户已经购买过一次");}//7.扣减库存boolean success = iSeckillVoucherService.update().setSql("stock =stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {//扣减失败return Result.fail("库存不足!");}//8.创建订单VoucherOrder voucherOrder = new VoucherOrder();//8.1 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//8.2 用户idvoucherOrder.setUserId(userId);//8.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 9.返回订单idreturn Result.ok(orderId);}

解决Redis分布式锁误删问题:

需求:修改之前的分布式锁实现,满足:

  1. 在获取锁时存入线程标示(可以用UUID表示)
  2. 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁
public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;//锁的名称private String name;public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}//锁的前缀private static final String KEY_PREFIX ="lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程表示String threadId =ID_PREFIX+Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//获取线程标示String threadId =ID_PREFIX+Thread.currentThread().getId();// 获取锁中的标示String id =stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);//判断标示是否一致if (threadId.equals(id)){//释放锁stringRedisTemplate.delete(KEY_PREFIX+name);}}
}

分布式锁的原子性问题:

当获取锁标示并判断是一致时,jvm执行gc时改线程发生阻塞,导致没有及时释放锁。如果在阻塞阶段锁超时释放,就会导致其他线程获得到锁。这时如果改线程阻塞结束,去释放锁就会导致误释放其他线程的锁。引发线程安全问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5tsvF7P-1668929831833)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221117175732336.png)]

解决方法:使判断锁和释放锁为原子性(同成功,同时失败)

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多余Redis命令,确保多条命令执行时的原子性。

Redis提供的调用函数,语法如下:

redis.call('命令','key','其它参数',...)

例如,执行set name jack 脚本为:

redis.cll('set','name','jack')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0l9SJuP-1668929831834)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221117233539943.png)]

再次改进Redis的分布式锁:

需求:基于Lua脚本实现分布式锁的释放锁逻辑

提示:RedisTemplate调用Lua脚本的API如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DjreDlCR-1668929831834)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118163922608.png)]

释放锁的逻辑改变

  private static final DefaultRedisScript UNLOCK_SCRIPT ;static {UNLOCK_SCRIPT =new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}  
@Overridepublic void unlock() {//调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());}

lua脚本:

-- 比较线程标示与锁中的标示是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then--释放锁 del keyreturn redis.call('del',KEYS[1])
end
return 0

总结基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放。避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化:

基于setnx实现的分布式锁存在下面的问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A72BP6zL-1668929831835)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118174134161.png)]

Redisson:

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzZTyMPi-1668929831835)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118174520032.png)]

Redisson快速入门:

1.引入依赖:

        org.redissonredisson3.13.6

2.配置Redisson客户端:

@Configuration
public class RedisConfig {@Beanpublic RedissonClient redissonClient(){//配置类Config config=new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://43.138.50.132:6379").setPassword("123321");//创建客户端return Redisson.create(config);}
}

3.使用Redisson的分布式锁

 @Resourceprivate RedissonClient redissonClient;@Testvoid testRedisson() throws InterruptedException{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock =lock.tryLock(1,10, TimeUnit.SECONDS);//判断释放获取成功if (isLock){try {System.out.println("执行业务");}finally {//释放锁lock.unlock();}}}

Redisson可重入锁原理:

锁的存储使用hash结构

获取锁:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwMMZ49P-1668929831836)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118230056580.png)]

释放锁:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfevVvyF-1668929831837)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221118230253311.png)]

基于setnx实现的分布式锁存在下面的问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yG4MSpoo-1668929831838)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221120143212767.png)]

Redisson分布式锁原理:

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

Redisson的multiLock解决:

分布式锁主从一致性问题----没听懂。。。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0oAlqch-1668929831839)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221120152726282.png)]

总结:

不可重入Redis分布式锁:

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入,无法重试,锁超时失效

可重入的Redis分布式锁:

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制重试等待
  • 缺陷:redis宕机引起锁失效问题

Redisson的multiLock:

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...