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

4.分布式锁

4.0 问题

4.0.1 事务失效

  1. 1. 事务失效问题

当在同一个类中调用带有 @Transactional 注解的方法时,Spring 的事务机制可能会失效。这是因为:

  • Spring 的事务管理是通过 AOP(面向切面编程)实现的
  • 当一个方法内部直接调用同类中的另一个带有 @Transactional 注解的方法时,实际上是在调用原始对象的方法,而不是代理对象的方法
  • 由于事务是由代理对象管理的,所以直接调用原始对象的方法会导致事务注解失效
  1. 2. 代理对象与目标对象

在 Spring AOP 中:

  • 目标对象 :实际的业务类实例(原始对象)
  • 代理对象 :Spring 创建的包装了目标对象的代理,用于实现事务等增强功能

当外部调用一个带有 @Transactional 注解的方法时,实际上是调用的代理对象的方法,代理会在调用目标方法前后添加事务管理的代码。

4.0.2 分布式锁误删

使用 UUID 而不使用 ThreadID 的原因:

  • ThreadID 在每个 JVM 内 ThreadID 是自增的,分布式下会有相同的 ThreadID
  • 所以不能让 ThreadID 作为 value 来做是否为同一把锁的标识

4.1 基本原理

  • 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
  • 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

4.1.1 满足条件

那么分布式锁他应该满足一些什么样的条件呢?

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

4.1.2 三种实现

  • Mysql:mysql 本身就带有锁机制,但是由于 mysql 性能本身一般,所以采用分布式锁的情况下,其实使用 mysql 作为分布式锁比较少见
  • Redis:redis 作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用 redis 或者 zookeeper 作为分布式锁,利用 setnx 这个方法,如果插入 key 成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
  • Zookeeper:zookeeper 也是企业级开发中较好的一个实现分布式锁的方案,由于本套视频并不讲解 zookeeper 的原理和分布式锁的实现,所以不过多阐述

4.2 实现思路

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

  • 获取锁:
    • 互斥:确保只能有一个线程获取锁
    • 非阻塞:尝试一次,成功返回 true,失败返回 false
  • 释放锁:
    • 手动释放
    • 超时释放:获取锁时添加一个超时时间

核心思路:

  • 我们利用 redis 的 setNx 方法
  • 当有多个线程进入时
  • 我们就利用该方法
  • 第一个线程进入时,redis 中就有这个 key 了,返回了 1
  • 如果结果是 1,则表示他抢到了锁,那么他去执行业务
  • 然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

4.3 V1-setnx 分布式锁

4.3.1 代码

private static final String KEY_PREFIX="lock:"
@Override
public 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);
}
public 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();}}

4.3.2 问题

逻辑说明:

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

解决方案:

(线程 1,2 都是该用户的请求)

  • 解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己
  • 如果属于自己,则不进行锁的删除
  • 假设还是上边的情况,线程 1 卡顿,锁自动释放
  • 线程 2 进入到锁的内部执行逻辑,此时线程 1 反应过来,然后删除锁
  • 但是线程 1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑
  • 当线程 2 走到删除锁逻辑时,如果没有卡过自动释放锁的时间点
  • 则判断当前这把锁是属于自己的,于是删除这把锁。

4.4 V2-解决误删问题

需求:修改之前的分布式锁实现,满足:在获取锁时存入线程标示(可以用 UUID 表示)

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

  • 如果一致则释放锁
  • 如果不一致则不释放锁

核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

4.4.1 代码

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

有关代码实操说明:

  • 在我们修改完此处代码后,我们重启工程
  • 然后启动两个线程
  • 第一个线程持有锁后,手动释放锁
  • 第二个线程 此时进入到锁内部,再放行第一个线程
  • 此时第一个线程由于锁的 value 值并非是自己
  • 所以不能释放锁,也就无法删除别人的锁
  • 此时第二个线程能够正确释放锁
  • 通过这个案例初步说明我们解决了锁误删的问题。

4.4.2 问题

更为极端的误删逻辑说明:

  • 线程 1 现在持有锁之后,在执行业务逻辑过程中
  • 他正准备删除锁,而且已经走到了条件判断的过程中
  • 比如他已经拿到了当前这把锁确实是属于他自己的
  • 正准备删除锁,但是此时他的锁到期了
  • 那么此时线程 2 进来,但是线程 1 他会接着往后执行
  • 当他卡顿结束后,他直接就会执行删除锁那行代码
  • 相当于条件判断并没有起到作用,这就是删锁时的原子性问题
  • 之所以有这个问题,是因为线程 1 的拿锁,比锁,删锁,实际上并不是原子性的
  • 我们要防止刚才的情况发生

4.5 V3-Lua 解决删锁原子性问题

我们来回想一下我们释放锁的逻辑:

  1. 1. 获取锁中的线程标示
  2. 2. 判断是否与指定的标示(当前线程标示)一致
  3. 3. 如果一致则释放锁(删除)
  4. 4. 如果不一致则什么都不做
-- 这里的 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 脚本

我们的 RedisTemplate 中,可以利用 execute 方法去执行 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());
}
经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~

4.6 小总结

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

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

    - 利用 set nx 满足互斥性

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

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

笔者总结:我们一路走来,利用添加过期时间,防止死锁问题的发生,但是有了过期时间之后,可能出现误删别人锁的问题,这个问题我们开始是利用删之前 通过拿锁,比锁,删锁这个逻辑来解决的,也就是删之前判断一下当前这把锁是否是属于自己的,但是现在还有原子性问题,也就是我们没法保证拿锁比锁删锁是一个原子性的动作,最后通过 lua 表达式来解决这个问题

但是目前还剩下一个问题锁不住,什么是锁不住呢,你想一想,如果当过期时间到了之后,我们可以给他续期一下,比如续个 30s,就好像是网吧上网,网费到了之后,然后说,来,网管,再给我来 10 块的,是不是后边的问题都不会发生了,那么续期问题怎么解决呢,可以依赖于我们接下来要学习 redission 啦

测试逻辑:

第一个线程进来,得到了锁,手动删除锁,模拟锁超时了,其他线程会执行 lua 来抢锁,当第一条线程利用 lua 删除锁时,lua 能保证他不能删除其他的锁,第二个线程删除锁时,利用 lua 同样可以保证不会删除别人的锁,同时还能保证原子性。

http://www.xdnf.cn/news/5187.html

相关文章:

  • C++进阶--AVL树的实现续
  • HC-SR04超声波测距传感器
  • Doris和Clickhouse对比
  • 视觉革命来袭!ComfyUI-LTXVideo 让视频创作更高效
  • Kotlin知识体系(七) : Flow线程控制、状态管理及异常处理指南
  • 每日脚本学习5.10 - XOR脚本
  • SSH终端登录与网络共享
  • AI与机器人学:从SLAM到导航的未来
  • HTTP/3展望、我应该迁移到HTTP/2吗
  • 【Linux】线程的同步与互斥
  • 物联网之使用Vertx实现MQTT-Server最佳实践【响应式】
  • 互联网大厂Java面试实录:Spring Boot与微服务架构在电商场景中的应用解析
  • MIT XV6 - 1.4 Lab: Xv6 and Unix utilities - find
  • vllm笔记
  • Linux510 ssh服务 ssh连接
  • 数学证明 | 逻辑的力量
  • 每天五分钟机器学习:拉格朗日对偶函数
  • 2025年渗透测试面试题总结-渗透测试红队面试三(题目+回答)
  • Pandas:数据处理与分析
  • 操作系统实验习题解析 上篇
  • UniRepLknet助力YOLOv8:高效特征提取与目标检测性能优化
  • 什么是静态住宅IP?为什么静态住宅IP能提高注册通过率?
  • 【部署】win10的wsl环境下调试dify的api后端服务
  • PyTorch API 2 - 混合精度、微分、cpu、cuda、可视化
  • torch.nn 下的常用深度学习函数
  • uniapp-商城-48-后台 分类数据添加修改弹窗bug
  • Kubernetes 使用 containerd 实现 GPU 支持及 GPU Operator 部署指南
  • Eclipse 插件开发 6 右键菜单
  • 从 JMS 到 ActiveMQ:API 设计与扩展机制分析(三)
  • 单脉冲前视成像多目标分辨算法——论文阅读