当前位置: 首页 > news >正文

Redis7-分布式锁

目录

基本原理

分布式锁的实现

基于Redis的分布式锁

Redis分布式锁误删

分布式锁的原子性问题

基于Redis的分布式锁优化

Redission概述

Redisson入门

Redisson可重入锁原理

​编辑

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

Redission的MultiLock原理

分布式锁总结


基本原理

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

分布式锁需要满足的条件:

  • 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化
  • 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
  • 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
  • 高性能:由于加锁本身就让性能降低,对于分布式锁本身需要有较高的加锁性能和释放锁性能
  • 安全性

分布式锁的实现

分布式锁的核心是实现多线程之间互斥,常见的三种实现方式:

基于Redis的分布式锁

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

1.获取锁

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

  • 非阻塞:尝试一次,成功返回true,失败返回false

2.释放锁

  • 手动释放

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

流程: 

代码实现:

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

public class SimpleRedisLock implements ILock{private static final String KEY_PREFIX="lock:";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标示String threadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//通过del删除锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}
    @Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象(新增代码)SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁对象boolean isLock = lock.tryLock(1200);//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

Redis分布式锁误删

持有锁的线程在锁的内部出现了阻塞,导致它的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁的情况说明

解决方案:在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

需求:

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

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

  • 如果一致则释放锁

  • 如果不一致则不释放锁

代码实现:

获取锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public 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);
}

释放锁

public 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);}
}

分布式锁的原子性问题

线程1现在持有锁之后,在执行业务逻辑过程中,正准备删除锁,而且已经走到了条件判断的过程中,比如它已经拿到了当前这把锁,确实是属于自己的,正准备删除锁,但是此时它的锁到期了,那么此时线程2进来,但是线程1它会接着往后执行,当它卡顿结束后,它直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题

Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法:Lua 教程 | 菜鸟教程

Redis提供的调用函数:

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

例:执行set name jack

# 执行 set name jack
redis.call('set', 'name', 'jack')

例:先执行set name Rose,再执行get name

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

例:执行 redis.call('set', 'name', 'jack') 这个脚本

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:  

Lua脚本实现释放锁:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,则删除锁return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

利用Java代码调用Lua脚本改造分布式锁

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

总结

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

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性

  • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化

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

Redission概述

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

Redission提供了分布式锁的多种多样的功能:

官网:https://redisson.org

Redisson入门

1.引入依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version>
</dependency>

2.配置Redisson客户端:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");// 创建RedissonClient对象return Redisson.create(config);}
}

3.使用Redission的分布式锁:

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

4.在 VoucherOrderServiceImpl注入RedissonClient:

@Resource
private RedissonClient redissonClient;@Override
public Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否已经结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock() < 1) {// 库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();//创建锁对象 使用分布式锁RLock lock = redissonClient.getLock("lock:order:" + userId);//获取锁对象boolean isLock = lock.tryLock();//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

Redisson可重入锁原理

Reddisson锁重试和WatchDog机制

Redisson分布式锁原理

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用whtchDog,每隔一段时间(releaseTime / 3),重置超时时间

Redission的MultiLock原理

为了提高redis的可用性,会搭建集群或者主从,以主从为例

此时去写命令,写在主机上, 主机会将数据同步给从机,但是假设主机还没有来得及把数据写入到从机时,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,锁信息已经丢掉了

为了解决这个问题,Redission提出来了MultiLock锁,使用这把锁就不再使用主从,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么它去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

MultiLock加锁原理:

当设置了多个锁时,Redission会将多个锁添加到一个集合中,然后用while循环不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试

分布式锁总结

1.不可重入Redis分布式锁

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

2.可重入的Redis分布式锁

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

3.Redisson的MultiLock

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 嵌入式学习Day30---Linux软件编程---进程间的通信
  • 网络通信(TCP/UDP协议 三次握手四次挥手 )
  • Webpack Bundle Analysis:减少包体积的技巧
  • Linux——进程(2)
  • IO多路复用—前言
  • 【OneAPI】中国行政区域省市县编码查询
  • 集成学习:融合多个模型
  • 负载均衡之HAProxy超全内容!!!
  • SDL 与 OpenGL 的关系
  • Vue3学习 Day01
  • 张量补充 2 (补充ing)
  • WPF使用LibVLC.WPF进行本地视频文件播放
  • 【CTF | WEB】003、攻防世界WEB题目之xff_referer
  • 设计模式-享元模式
  • HTTP 之 头部信息(二)
  • [deviceone开发]-do_Webview的基本示例
  • IndexedDB
  • React的组件模式
  • 构建二叉树进行数值数组的去重及优化
  • 解析 Webpack中import、require、按需加载的执行过程
  • 开发了一款写作软件(OSX,Windows),附带Electron开发指南
  • 浅谈web中前端模板引擎的使用
  • 如何使用 JavaScript 解析 URL
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 小程序 setData 学问多
  • 译米田引理
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • ​学习笔记——动态路由——IS-IS中间系统到中间系统(报文/TLV)​
  • #define、const、typedef的差别
  • #Linux(make工具和makefile文件以及makefile语法)
  • $.proxy和$.extend
  • (16)Reactor的测试——响应式Spring的道法术器
  • (24)(24.1) FPV和仿真的机载OSD(三)
  • (42)STM32——LCD显示屏实验笔记
  • (el-Date-Picker)操作(不使用 ts):Element-plus 中 DatePicker 组件的使用及输出想要日期格式需求的解决过程
  • (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
  • (Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一
  • (第8天)保姆级 PL/SQL Developer 安装与配置
  • (二)linux使用docker容器运行mysql
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • (附源码)计算机毕业设计大学生兼职系统
  • (三)mysql_MYSQL(三)
  • (四)js前端开发中设计模式之工厂方法模式
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • (自用)learnOpenGL学习总结-高级OpenGL-抗锯齿
  • ***详解账号泄露:全球约1亿用户已泄露
  • .mkp勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET CLR基本术语
  • .NET 简介:跨平台、开源、高性能的开发平台
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .net开发引用程序集提示没有强名称的解决办法
  • .NET设计模式(8):适配器模式(Adapter Pattern)
  • @Autowired和@Resource装配
  • @column注解_MyBatis注解开发 -MyBatis(15)