分布式锁优化:使用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获得锁,执行业务时卡住了(比如GC阻塞或垃圾回收机制)。
- 锁超时释放了,线程2趁机获得了锁。
- 就在这时线程1恢复了,执行
delete()
操作,误删了线程2的锁。 - 于是线程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的keyARGV数组
:代表传入的参数
四、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脚本后,我们可以通过StringRedisTemplate
的execute()
方法来执行这个脚本。
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,但是我觉得再牛逼的框架也是从底层这样写出来的,如果不打好基础,光会安装车轮子又有什么用呢,迟早会被淘汰。