专项攻克——redis分布式锁-用LUA脚本解决setNX锁不释放问题

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

  1. Lua脚本在Redis中是原子执行,整个脚本执行期间不会被其它命令打断。
  2. 解锁两件事要原子做:①判断锁的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脚本(重点,防止锁不释放 + 防止误删别人锁)

脚本逻辑:

  1. 获取锁key的value

  2. 判断和传入的当前客户端uuid是否相等

  3. 相等才删除,不相等直接返回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是两条命令,中间存在时间窗口:

  1. 线程A get发现是自己的锁
  2. 刚好锁超时,redis自动过期删除,线程B拿到同一个锁
  3. 线程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。

相关推荐
rustfs3 小时前
MinIO 国产开源平替正式 GA
分布式·docker·云原生·rust
螺蛳粉 螺蛳粉5 小时前
MySQL 分布式集群系列 · 第五篇——全方位对比:NDB、MGR、主从复制、分库分表怎么选?
数据库·分布式·mysql
孙启超7 小时前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
九皇叔叔9 小时前
Seata——把分布式事务理论落到 Java 微服务实践
分布式·分布式事务·cap·base·saga
Nano叶落10 小时前
几十万个-key-扫不过来?-bigkeys、SCAN、RDB-离线分析三步定位最该清的-Big-Key
redis
shark-chili11 小时前
关于AI辅助编程的认知
数据库·人工智能·redis·macos·缓存
heimeiyingwang13 小时前
【中台·技术篇】缓存中台建设:Redis 集群统一管理与多级缓存架构
redis·中台
晚安日记wanna14 小时前
一条 SMEMBERS 干瘫 Redis 节点:单线程的真正边界在哪
redis·后端·面试
程序员黎剑14 小时前
RabbitMQ-消息可靠性-publisher-confirm持久化与手动ack
分布式·rabbitmq·ruby
程序猿乐锅15 小时前
【黑马点评 | 第二篇】Redis 缓存更新策略与商铺缓存实现
java·网络·redis·spring·mybatis