Redis分布式锁 | 黑马点评
目录
一、分布式锁概述
二、基于Redis的分布式锁
1、思路分析
2、初级版本
3、误删问题
4、改进分布式锁
5、原子性问题
6、使用Lua脚本解决原子性问题
7、setnx实现分布式锁存在问题
三、Redisson
1、Redisson快速入门
2、Redisson可重入锁原理
3、Redisson可重试原理
4、Redisson解决超时问题
5、Redission主从一致性问题
四、总结
一、分布式锁概述
在集群模式下,synchronize根本锁不住。因为每个都是不同tomcat,不同jvm的存在,每个jvm的每个锁都可以有一个线程来获取,就会出现并行安全问题。
要想解决这种问题,必须得想办法让多个jvm只能用同一个锁。分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。让多个jvm进程都可以看到锁监视器,而且只有一个进程可以拿到锁。
特点:多进程可见、互斥、高可用、高性能、安全性
二、基于Redis的分布式锁
1、思路分析
实现分布式锁的两个基本方法:
我们获取锁的方法可能会出现问题,当我们添加完锁之后,还没来得及设置时间突然宕机,这个时候就死锁了。所以我们必须保证添加锁和设置时间的时候要原子性,一起完成。
我们可以把获取锁的两步修改为:(NX代表互斥,EX是设置超时时间)
#添加锁,NX是互斥,EX是设置超时时间
SET lock thread1 NX EX 10
2、初级版本
需求:定义一个类,实现下面接口,利用Redis分布式锁实现一个用户只能下一单的业务
public interface ILock {/*** 尝试获取锁* @param timeoutSec 锁持有的超时时间,过期后自动释放* @return true代表获取锁成功;false代表获取锁失败*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}
实现ILock接口:
name属性:我们希望不同的业务有不同的锁,name是业务的名称
锁的参数:key是lock:业务名称,value是锁的线程的id
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);}
}
业务层调用,实现一人只能下一单
我们这里构造参数传入的时候name不仅仅传入业务名称了,还要加上用户id,因为只是锁一个用户下一单,同一个用户才加锁。所以传入“order:”+userId
@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一下,异常的话,finally释放锁try {//获取spring事务代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} catch (IllegalStateException e) {e.printStackTrace();}finally {//释放锁lock.unlock();}
// }return Result.fail("抢购失败");}
3、误删问题
当线程1正确获取redis锁执行业务,业务某种原因阻塞,超过时间就会超时释放,一旦1提前释放,2能获取成功,当2正在拿锁做业务时,1突然醒了然后执行完毕释放锁,这个时候1就把2的锁给释放掉了,3或者其他线程就能够进来。
这种情况产生主要是因为线程1把线程2的锁给释放了导致,我们可以在释放锁之前加上判断是否是自己的锁,我们可以把redis存入的value来当这个线程的标识,删除的时候取出来判断一下
4、改进分布式锁
修改之前的分布式锁,在获取锁的时候存入线程的标识(可以是UUID标识)
为什么要用UUID来标识呢,我们之前用线程id为什么不行?
因为线程id是递增的,每个jvm都是这样递增,所以不同的tomcat最终线程id是可能相同的
在释放的时候先判断线程是否一致,一致释放,不一致不释放。
获取锁和释放锁的方法改进:
我们这里采用UUID+线程id当做redis的value
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);}}
}
UUID是常量,同一个线程生成的UUID是一样的,所以可以这样写,我们的value还加上的线程id
因为单UUID还是可能会重复的,只是概率特别小,再加上线程id,就不太可能重复了。
5、原子性问题
这种情况:当线程1获取锁执行业务,完成业务释放锁判断标识一致可以释放,这个时候判断完成堵塞了(可能是jvm垃圾回收会阻塞所有的代码),就没有释放成功,如果足够长,就触发了超时释放锁。一旦超时释放,其他线程2就能获取锁然后执行业务,这个时候线程1恢复了,但是他已经判断完标识了,这个时候直接又把2的锁给释放了,然后线程3又能进来执行
所以我们必须保证判断锁和释放锁的原子性。
6、使用Lua脚本解决原子性问题
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性
lua脚本是用Lua是一种编程语言,我们只要会用基础的操作就可以了
再次改进Redis的分布式锁:
需求:基于Lua脚本实现分布式锁的释放锁逻辑
释放锁的逻辑改变
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT ;static {UNLOCK_SCRIPT =new DefaultRedisScript<>();//设置脚本的路径,就在resource目录下直接名字就可以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
7、setnx实现分布式锁存在问题
不可重入:线程1先调用A获得锁,又要调用B方法,需要获取同一把锁,如果锁是不可重入的,方法A又没有释放锁,这个时候就会出现死锁问题。
不可重试:我们之前的是没有拿掉锁立刻失败返回false,我们有时候需要这个锁被别人获取拿不到,我们就等等,如果最后成功了我们再执行业务。
超时释放:太短的话可能没执行完业务就放,太长可能出问题了等待时间较长
主从一致性:redis主从之间同步是存在延迟的,线程1在主节点获得了锁,尚未同步给从节点的时候,突然主节点宕机,但是替换的从节点没有锁的,这个时候其他线程都可以拿到锁了。但是这种情况概率比较低,主从的延迟是极低的。
要解决上面这些问题就非常麻烦了,所以我们要借助成熟的框架Redisson
三、Redisson
Redisson是Redis的基础上实现的Java驻内存数据网格。不仅提供了一系列分布式java常用对象,还提供了许多分布式服务,其中就包含了分布式锁的实现。
1、Redisson快速入门
(1)引入Redisson依赖
(2)配置Redisson客户端
(3)使用Redisson的分布式锁
2、Redisson可重入锁原理
我们自己实现的分布所锁不能可重入,为什么redisson的可以呢,底层怎么实现呢?
如图,他底层锁的是用的redis的hash结构,因为还要存个进入锁的次数,key是之前业务名称,value分别是线程的唯一标识和进入锁的次数
当每次进入就判断线程是不是之前的线程并让统计数+1,执行完业务就让统计次数-1,如果统计数为0,说明要释放锁了。
因为操作很多,所有我们要保证原子性必须用lua脚本来写
获得锁的lua脚本
释放锁的lua脚本
3、Redisson可重试原理
他底层在拿不到锁的时候并没有直接结束,而是订阅别人释放锁的信号。
在源码lua中每当有锁释放的时候都会发出异步消息告诉别人已经释放锁了。
所有有人真的释放锁应该会发消息过来,我们收到消息再重试。
这个等待多久时间是我们设置的最大等待时间,等到最大剩余时间结束了还没拿到锁,那就取消订阅返回false。这种是等待释放了再尝试,不是一直尝试减少了cpu的消耗
4、Redisson解决超时问题
利用看门狗机制,就是在获取成功之后,开启一个看门狗的定时任务,每隔一段时间就会重置锁的超时时间。
5、Redission主从一致性问题
主从分离,主节点来写操作,从节点来读操作,为了保证数据一致性,所有要进行主从同步
但是主从同步会有一定的延迟,可能就会产生问题
如果主节点修改完之后还没来得及同步瞬间挂了,这就是产生了主从一致性问题
redission的解决策略就很简单,直接开三台redis同时来写和读,把3台redis的锁合在一起做连锁,也可以3台机器都做主从分离也可以不建立。
四、总结
Redis分布式锁 | 黑马点评相关推荐
- Redis分布式锁的实现以及原理
1 前言 在程序中,我们想要保证一个变量的可见性及原子性,我们可以用volatile(对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性).sync ...
- 【Redis】Redis实战:黑马点评之优惠券秒杀
Redis实战:黑马点评之优惠券秒杀 1 全局唯一ID 1.1全局唯一ID 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据 ...
- redis分布式锁 在集群模式下如何实现_收藏慢慢看系列:简洁实用的Redis分布式锁用法...
在微服务中很多情况下需要使用到分布式锁功能,而目前比较常见的方案是通过Redis来实现分布式锁,网上关于分布式锁的实现方式有很多,早期主要是基于Redisson等客户端,但在Spring Boot2. ...
- 快来学习Redis 分布式锁的背后原理
以前在学校做小项目的时候,用到Redis,基本也只是用来当作缓存.可阿粉在工作中发现,Redis在生产中并不只是当作缓存这么简单.在阿粉接触到的项目中,Redis起到了一个分布式锁的作用,具体情况是这 ...
- Redis分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!
点击关注公众号,Java干货及时送达 来源:juejin.cn/post/6854573212831842311 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本篇文章主要是基于我们实际项目 ...
- Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!
点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本 ...
- 秒杀商品超卖事故:Redis分布式锁请慎用!
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 来源:juejin.im/post/6854573 ...
- 记一次由Redis分布式锁造成的重大事故,避免以后踩坑!
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:浪漫先生 juejin.im/post/5f159cd8f2 ...
- 简单介绍redis分布式锁解决表单重复提交的问题
在系统中,有些接口如果重复提交,可能会造成脏数据或者其他的严重的问题,所以我们一般会对与数据库有交互的接口进行重复处理.本文就详细的介绍一下redis分布式锁解决表单重复提交,感兴趣的可以了解一下 假 ...
最新文章
- 设计一个魔方(六面)的程序 【微软面试100题 第四十四题】
- Swing实现全屏(覆盖任务栏和不覆盖任务栏)
- vue filter对象_vue 过滤器
- Pytorch实战1:线性回归(Linear Regresion)
- java试讲题目,常见的Java面试题汇总
- centos部署时间服务器
- 更改ubuntu的mysql版本为指定版本
- 2021-10-11 CTF-KX(第一场)-RSA10
- QQ音乐接口api,包括付费音乐、无损音乐、高品质音乐地址解析接口api
- TableWidget表格绘制常用函数
- 洛谷 P4093 [HEOI2016/TJOI2016]序列(Cdq+dp)
- 汉语词典快速查询算法研究
- Flink运行时架构及各部署模式下作业提交流程
- JVM之 方法区、永久代(PermGen space)、元空间(Metaspace)三者的区别
- 计算机原理与智能-翻译
- 后台站点-菜单管理功能(一)
- 小白成长记第2期:简单易操的YouTube美金项目,get!
- 手把手教你应用三种工厂模式在SpringIOC中创建对象实例【案例详解】
- mc经常闪退是java有问题_【疑问】求大神回答,mc闪退,提供崩溃报告
- ssas脚本组织程序_SSAS中的MDX脚本
热门文章
- HTML class类名企业命名参考
- WebView 初始化失败
- c语言考试填空题不删横线,2015年计算机二级考试《C语言》提高练习题(11)
- 中国鸣网-最专业、快捷的学术期刊征稿、期刊投稿、杂志征稿平台
- 5G NR双激活协议栈(DAPS)~导入
- 【艾琪出品】《计算机应用基础》【试题汇总3】《多媒体技术》《网页设计与制作》《电子商务网站设计与管理》《数据库原理》
- 最牛逼的 Java 日志框架,性能无敌,横扫所有对手。。
- android仿微博首页布局,Android仿微博首页Tab加号弹窗功能
- 中职学校计算机课教学模式,最新中职学校计算机课程教学模式和方法探索
- IE 浏览器已“死”,一个时代的终结