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

分布式锁优化:使用Lua脚本保证释放锁的原子性问题

分布式锁优化(二):使用Lua脚本保证释放锁的原子性问题

💻黑马视频链接:Lua脚本解决多条命令原子性问题

在上一章节视频实现了一个可用的Redis分布式锁,采用SET NX EX命令实现互斥和过期自动释放机制,并通过给锁绑定线程标识来避免锁误删的问题。

正当我已经感觉非常完美地防止“超卖”问题的时候,虎哥又举了一个更小概率的事件哈哈,判断和删除是两次单独的Redis操作,中间如果线程阻塞或者上下文切换,就可能导致“误删别人的锁”! 于是谈到:释放锁操作的原子性问题。下面将继续优化这把锁,引入Lua脚本,彻底解决这个问题(其实还是不够彻底哈哈~)

一、释放锁不是原子操作

之前我们是这样释放锁的:

String id = redisTemplate.opsForValue().get("lock:order");
if (id.equals(currentThreadId)) {redisTemplate.delete("lock:order");
}

这看起来没问题对吧?我们先判断锁是不是自己的,如果是就删掉。

但问题就在于:两次单独的Redis操作,中间如果阻塞,就可能导致“误删别人的锁”。(如下图)
在这里插入图片描述

举个真实的例子:

  1. 线程1获得锁,执行业务时卡住了(比如GC阻塞或垃圾回收机制)。
  2. 锁超时释放了,线程2趁机获得了锁。
  3. 就在这时线程1恢复了,执行delete()操作,误删了线程2的锁。
  4. 于是线程3也抢到了锁,导致并发执行 → 超卖!

这就像网吧上机,座位是你在用没错,但你出去上厕所的功夫别人坐了你的位置,你回来不管三七二十一直接拔网线……

二、我们需要“原子释放锁”

原子性:一组操作要么全部完成,要么全部不做,中间不允许被打断。

我们需要将“比对线程标识”和“删除锁”这两个操作合并成一个原子操作,要么同时完成,要么都不执行。这样才能避免误删问题。

Redis 本身虽然没有支持“if equals then delete”这种原子命令,但它提供了一种机制——Lua脚本

三、Lua脚本简介:Redis的原子武器

Lua是一门轻量级脚本语言,Redis支持在服务端执行Lua脚本,一旦脚本开始执行,就不会被任何其他命令打断,具有绝对的原子性。
在这里插入图片描述

这就像我们把所有关键操作包成一个“事务”扔给Redis执行,Redis承诺要么一次性全完成,要么一个都不做,其他客户端在这期间不能插队。

常用语法:

-- 获取值
local val = redis.call('GET', KEYS[1])-- 设置值
redis.call('SET', KEYS[1], ARGV[1])-- 删除
redis.call('DEL', KEYS[1])
  • KEYS数组:代表Redis的key
  • ARGV数组:代表传入的参数

四、Lua脚本释放锁:拿锁-比锁-删锁三步走

我们可以这样写一个Lua脚本,来实现释放锁逻辑:

-- unlock.lua
-- 如果锁的值(线程ID)等于传入的线程ID,则删除锁
if (redis.call('GET', KEYS[1]) == ARGV[1]) thenreturn redis.call('DEL', KEYS[1])
end
return 0

这段脚本的执行具备原子性,确保:
拿锁 → 比锁 → 删除锁三个操作连成一体
中间不可能被打断或抢占

五、Java代码实现:调用Lua脚本

有了Lua脚本后,我们可以通过StringRedisTemplateexecute()方法来执行这个脚本。

1. Lua脚本保存

将上面的unlock.lua文件放在项目的resources/lua/目录下。

2. 加载Lua脚本

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("lua/unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class); // 返回值类型:DEL成功返回1,失败返回0
}

3. 释放锁的代码

@Override
public void unlock() {stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name), // 传入锁的key(KEYS数组)ID_PREFIX + Thread.currentThread().getId() // 传入线程ID(ARGV数组));
}

这样,我们的释放锁操作就是一个原子动作了!

虽然我们的分布式锁已经趋近“安全”,但它依然还不够“强大”。

六、还存在的问题:锁不住!

1. 锁不可重入

一个线程A获取了锁,然后调用另一个需要同样锁的函数B,无法再次获取锁,会直接死锁。因为Redis认为这把锁已经被人拿了(确实是你,但你又想拿一次)。

2. 没有重试机制

调用tryLock()只尝试一次失败就返回false,如果当前锁刚好被别人占用,就会放弃。这对一些关键业务来说代价太高。

3. 过期释放导致锁丢失

比如业务逻辑执行慢了,锁还没用完就被Redis自动释放了!这时别人就能抢锁,出现数据错乱。
解决方案:锁续期机制(像网吧上网时到了时间,自动续租锁的时间)。

在最后,我想一句,可能这些东西在很多内行人看来都是白雪,因为市面了已经有许多现成的轮子可以用了,比如说Redisson,但是我觉得再牛逼的框架也是从底层这样写出来的,如果不打好基础,光会安装车轮子又有什么用呢,迟早会被淘汰。

http://www.lryc.cn/news/2397870.html

相关文章:

  • 电脑wifi显示已禁用怎么点都无法启用
  • 【FPGA开发】Ubuntu16.04环境下配置Vivado2018.3—附软件包
  • vue-seamless-scroll 结束从头开始,加延时后滚动
  • 不同的数据库操作方式:MongoDB(NoSQL)和 MySQL/SQL
  • 0-EATSA-GNN:基于图节点分类师生机制的边缘感知和两阶段注意力增强图神经网络(code)
  • 大数据学习(124)-spark数据倾斜
  • 配置前端控制器
  • lua注意事项
  • Git的三种合并方式
  • 从零到一:我的技术博客导航(持续更新)
  • SpringBoot整合Flowable【08】- 前后端如何交互
  • DM达梦数据库开启SQL日志记录功能
  • 00 QEMU源码分析中文注释与架构讲解(v8.2.4版本)
  • 【五模型时间序列预测对比】Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN
  • 深入了解MCP基础与架构
  • 实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.13 R语言解题
  • 怎么选择合适的高防IP
  • 【java面试】MySQL篇
  • 贪心算法应用:欧拉路径(Fleury算法)详解
  • 【算法设计与分析】实验——二维0-1背包问题(算法分析题:算法思路),独立任务最优调度问题(算法实现题:实验过程,描述,小结)
  • P12592题解
  • ffmpeg命令(二):分解与复用命令
  • 【Git】View Submitted Updates——diff、show、log
  • deepseek原理和项目实战笔记2 -- deepseek核心架构
  • 在 MATLAB 2015a 中如何调用 Python
  • 房屋租赁系统 Java+Vue.js+SpringBoot,包括房屋类型、房屋信息、预约看房、合同信息、房屋报修、房屋评价、房主管理模块
  • 华为OD机试真题——生成哈夫曼树(2025B卷:100分)Java/python/JavaScript/C/C++/GO六种最佳实现
  • react与vue的渲染原理
  • 我提出结构学习的思路,意图用结构学习代替机器学习
  • Outbox模式:确保微服务间数据可靠交换的设计方案