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

分布式锁优化:使用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.xdnf.cn/news/767233.html

相关文章:

  • SQL进阶之旅 Day 12:分组聚合与HAVING高效应用
  • 微服务-Sentinel
  • Oracle expdp过滤部分表数据
  • vue-12 (路由守卫:全局、每个路由和组件内)
  • 【Unity】相机 Cameras
  • 项目管理进阶:56页大型IT项目管理实践经验分享【附全文阅读】
  • 数据库系统概论(十四)详细讲解SQL中空值的处理
  • Leetcode 2123. 使矩阵中的 1 互不相邻的最小操作数
  • 数据结构之堆:解析与应用
  • 高阶数据结构——并查集
  • vscode 插件 eslint, 检查 js 语法
  • mysql分布式教程
  • 构建高性能风控指标系统
  • AIGC工具平台-GPT-SoVITS-v4-TTS音频推理克隆
  • Arbitrum Stylus 合约实战 :Rust 实现 ERC721
  • Windows 账号管理与安全指南
  • Java后端优化:对象池模式解决高频ObjectMapper实例化问题及性能影响
  • SCAU8639--折半插入排序
  • JS手写代码篇---手写类型判断函数
  • Linux 基础指令入门指南:解锁命令行的实用密码
  • 无他相机:专业摄影,触手可及
  • 【C++高级主题】转换与多个基类
  • 电力系统时间同步系统
  • 玩客云 OEC/OECT 笔记(2) 运行RKNN程序
  • 一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录——4. 配置服务器终端环境 zsh , oh my zsh, vim
  • 数智管理学(十六)
  • 需求调研文档——日志文件error监控报警脚本
  • CSS强制div单行显示不换行
  • Qt/C++编写GB28181服务端工具/绿色版开箱即用/对标wvp-gb28181/实时画面预览/录像回放下载
  • 百度golang研发一面面经