Redis分布式锁:Lua脚本解决setNX锁不释放(面试版)
问题背景:
SETNX key value+EXPIRE key timeout分开两条命令,不是原子。
- setnx抢到锁,程序崩溃,还没执行expire → 锁永久不释放,死锁。
- 另外一个坑:线程A的锁超时,线程B拿到锁,A执行del把B的锁释放,锁误删除。
正确现代写法:
SET lock uuid NX EX 30一条原子命令加锁;但是解锁必须用Lua脚本保证原子,并且校验锁属于当前持有者,不能直接del。
核心为什么用Lua
- Lua脚本在Redis中是原子执行,整个脚本执行期间不会被其它命令打断。
- 解锁两件事要原子做:①判断锁的value是不是当前线程的UUID;②匹配才删除。
如果不用Lua:get lock判断 +del lock两条命令,中间会有时间窗口,会出现误删别人锁。
1、加锁(Java伪代码)
不要用 setnx + expire 分开!
// 生成唯一标识,UUID,代表当前客户端/线程
String lockValue = UUID.randomUUID().toString();
// NX:不存在才设置;EX:过期时间秒
String result = redisTemplate.opsForValue()
.setIfAbsent("lock:order", lockValue, 30, TimeUnit.SECONDS);
2、解锁Lua脚本(重点,防止锁不释放 + 防止误删别人锁)
脚本逻辑:
-
获取锁key的value
-
判断和传入的当前客户端uuid是否相等
-
相等才删除,不相等直接返回0,不做任何操作
-- unlock.lua
-- KEYS[1]:锁key lock:order
-- ARGV[1]:当前客户端唯一uuid
if redis.call('GET',KEYS[1]) == ARGV[1] then
return redis.call('DEL',KEYS[1])
else
return 0
end
- 返回
1:解锁成功;返回0:不是自己的锁,不删除。
⚠️这个脚本解决两个问题:
①不会删除别的线程持有的锁;
②判断+删除原子,不会中间出现线程切换。
Java调用Lua示例
String script = "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 执行脚本
Long res = redisTemplate.execute(redisScript, Collections.singletonList("lock:order"), lockValue);
if(res == 1){
System.out.println("解锁成功");
}
❓那锁超时没处理完业务怎么办?(锁自动过期带来的新问题)
Lua解锁脚本解决不了业务执行超过锁过期时间的问题 !
比如设置30s锁,业务跑了60s;锁自动过期释放,其他线程拿到锁,并发问题出现。
解决方案:看门狗(续期),Redisson已经封装。
- 拿到锁之后,后台线程每隔
过期时间/3(默认10s)续期重置过期时间; - 如果业务正常执行完毕,调用Lua解锁脚本,看门狗停止。
面试问答
Q1:setnx为什么会锁不释放?
setnx、expire是两条独立命令。如果setnx成功,服务宕机,expire永远执行不到,key没有过期时间,锁永久存在死锁。
解决方案:使用
SET key value NX EX time一条原子命令完成加锁+设置过期,加锁阶段就避免死锁。
Q2:解锁为什么不能先get再del,要Lua脚本?
get 和 del是两条命令,中间存在时间窗口:
- 线程A get发现是自己的锁
- 刚好锁超时,redis自动过期删除,线程B拿到同一个锁
- 线程A执行del,把B刚拿到的锁删掉>
造成锁误释放,并发安全失效。Lua脚本保证判断+删除整个逻辑原子执行。
Q3:Lua脚本能解决业务执行超过锁超时的问题吗?
不能。Lua只保证解锁逻辑原子。业务耗时大于锁过期时间,锁会自动释放,其他线程进来,会并发。要靠看门狗续期(Redisson)。
Q4:Redisson底层加解锁是不是Lua?
是的,Redisson加锁、解锁、锁续期全部使用Lua脚本保证原子性。
Q5:Redis分布式锁还存在什么缺陷?
主从集群场景:主节点写完锁,还没同步到从节点,主宕机,从晋升为主;锁丢失,出现并发。
解决方案:Redlock红锁(生产很少用,运维重);业务层面兜底。
面试一句话背诵版
使用
SET key uuid NX EX原子完成加锁设置过期,避免setnx成功宕机导致锁永不释放;解锁使用Lua脚本,原子完成校验锁持有者+删除,避免get+del非原子误删除别的线程锁;
Lua脚本无法解决业务执行超过锁过期时间,需要看门狗自动续期,生产推荐Redisson。