Redis分布式锁不止set nx那么简单

01 引言

在分布式的场景下,分布式锁是经常遇到的。而Redis的分布式锁用的较为频繁。但是Redis的分布式锁是怎么实现的呢?

在我们公司的业务代码中经常会出现这样的代码:

image-20251107140655066

或者

这样的代码用来当分布式锁或者处理幂等性等问题。这样有没有什么问题呢?

我想应该不止我们公司有人写成那样的代码吧!因为我们公司的并发不大,反正遇到的问题并不多。我们一起来探索一下吧!

02 分布式锁

分布式锁大家已经耳熟能详了,其核心特性无非就那么几个:

  • 互斥性:同一时刻只有一个客户端能持有锁
  • 安全性:锁只能由持有者释放,防止误删
  • 容错性:在部分节点故障时仍能正常工作
  • 避免死锁:必须有超时机制,防止客户端崩溃导致锁无法释放

我们根据以上特性分析一下上面代码中setnx+expireset可能会出现什么样的问题。

2.1 setnx + expire

我们还原一下伪代码:

let redisService = xxxxx;
try{
    if (1 == redisService.setnx(key, value)){
        redisService.expire(key, expire);
        // 后续业务....
    }
    
}finally {
    redisService.delete(key);          
}

setnx固然可以是保持互斥性,但是和expire方法是两个独立的方法。所以如果expire方法异常,就会导致setnx设为永久有效。虽然有redisService.delete兜底,但是如果出现服务器宕机,此处的代码几无法执行,而从造成死锁。

如果执行业务逻辑的时候由于网络抖动,分布式锁已经过期,业务还在阻塞中。这时第二个请求相同的Key就可以加锁了,业务逻辑还没有执行完,结果第一个方法的恢复正常执行了delete(key)的操作,就会误删掉第二个请求的key,也就是释放了第二个请求的锁。

还有就是由于业务复杂导致分布式锁过期,业务还没有执行完,就会自动释放锁。导致加锁失败。

简单总结问题:

  • setnxexpire没有原子性操作
  • 释放锁没有校验是不是自己的锁,可能会误删锁。
  • 锁没有续约

2.2 set

set的方法是setnx + expire的结合,属于原子性操作。我们来看看伪代码:

let redisService = xxxxx;
try{
    if ("OK" == redisService.setnx(key, value, "NX", "EX", 600)){
        // 后续业务....
    }
    
}finally {
    redisService.delete(key);          
}

这个方法比上面的好一点,保证了原子性。但是其他两个问题依然存在:

  • 释放锁没有校验是不是自己的锁,可能会误删锁。
  • 锁没有续约

2.3 优化

误删锁的问题容易解决,只需要在删除的时候,判断存入的Value是自己的Value即可。

String value = "xxxx";
if (value.equals(redisService.get(key)) {
    redisService.delete(key); 
}

锁的续约如何解决呢?我们耳熟能详的看门狗模式。简单来说就是启用一个线程,隔一段时间就去看当前客户端的Key有没有过期,快过期的时候,重新设置时间。

伪代码如下:

new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (redisService.expire(key) <= expire) {
                    // 续约
					redisService.expire(key, expire2);
                }
            }
        }, 10);

思路也是比较简单。但是难点就在续约的时间,检查锁是否过期的间隔。我们将通过几个开源案例了解一下具体的实现。

03 Lock4j

3.1 简介

Lock4j是一个分布式锁组件,其提供了多种不同的支持以满足不同性能和环境的需求,立志打造一个简单但富有内涵的分布式锁组件。其特性就是简单易用,功能强大,扩展性强,支持redission,redisTemplate,zookeeper。可混用,支持扩展。

该开源项目来自苞米豆社区,你可能没听过。但是Mybatis-Plus你一定听过,它的作者和Lock4j的作者是同一人。

3.2 RedisTemplate

StringRedisTemplateRedisTemplateSpringBoot自带的客户端。我们看看Lock4J是如何实现分布式锁的。

实现类:com.baomidou.lock.executor.RedisTemplateLockExecutor

从源码可以看出,对于分布式锁的加锁、释放锁以及续约,为了保证原子性,都是用了Lua脚本。

加锁

通过ForkJoin线程池启动线程,加锁后根据参数执行是否续约。然后等待加锁的结果,并返回加锁的结果。

释放锁

释放锁就比较简单了。根据Lua脚本,先查询对比,然后根据结果删除对应的key

续约

同样是先获取判断,然后在续约。都是通过Lua脚本处理的。但是要这里要说的是,通过new Timer()启动了一个调度任务,每到过期时间的三分之一,就会触发续约逻辑。

问题

我们会发现,在续约的时候启动的线程资源并没有被回收。如果加锁的线程很多,就会有很多任务在空跑。消耗了资源。小编在Gitee也返现了有人提出这个问题,并尝试优化:

https://gitee.com/baomidou/lock4j/issues/IBIE4A

3.3 Redisson

Redisson同样是一款经典的Redis客户端。该客户端自己实现分布式锁,并启用了看门狗模式。

加锁

直接通过RedissonClient客户端获取锁并尝试加锁,返回加锁结果。

释放锁

判断线程是否持有,有的话就释放锁。

续约

几乎没有代码。因为Redisson本身自带看门狗机制。我们追一下Redisson的源码。当执行lock()->tryAcquire()->tryAcquireAsync()->scheduleExpirationRenewal()->renewExpiration()

①我们可以看到依然通过TimerTask这样的调度任务。

②不续约的话就释放资源:

zookeeper的分布式锁我们暂且讨论,因为不是基于Redis的。

04 小结

分布式锁不是想象的那么简单,但也不是那么复杂。使用Redis分布式锁,直接Redssion客户端,使用完善的功能。使用RedisSet或者setnx,可以满足日常并发不大的场景,设置合理的过期时间,就能够满足大部分的场景了。

Redis 中实现分布式锁时,`SET NX EX` 是一个关键命令,它结合了多个参数以确保锁的设置是原子性的,从而避免并发竞争条件。具体来说,`SET NX EX` 是 `SET` 命令的扩展形式,其中: - `NX`(Not eXists)表示只有在目标 key 不存在的情况下才执行设置操作。 - `EX`(Expire seconds)用于指定 key 的过期时间(以秒为单位)。 通过 `SET lock_key unique_lock_value NX EX 10` 这样的命令,Redis 能够在 `lock_key` 不存在的前提下将其设置为指定值,并在 10 秒后自动过期。这一操作的原子性意味着设置 key 和设置过期时间不会被其他客户端的操作打断,从而保证了锁的安全性和一致性 [^2]。 ### 原子性的重要性 原子性确保了设置 key 和设置过期时间这两个操作要么同时成功,要么同时失败,不会出现中间状态。在分布式系统中,这种特性尤为重要,因为多个客户端可能同时尝试获取锁。`SET NX EX` 的原子性机制可以有效防止多个客户端同时获得锁,从而避免潜在的竞争条件 [^2]。 ### 锁的释放 在释放锁时,通常需要确保只有持有锁的客户端能够删除对应的 key。为了实现这一点,客户端在获取锁时会设置一个唯一值(如 `unique_lock_value`),在释放锁时通过 Lua 脚本来验证 key 的值是否匹配,只有匹配的情况下才删除 key。这种方式可以避免误删其他客户端持有的锁 [^2]。 Lua 脚本的使用进一步增强了锁释放的原子性。例如,以下 Lua 脚本可以用于安全地释放锁: ```lua if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end ``` 该脚本会检查当前 key 的值是否与预期的 `unique_lock_value` 一致,如果一致则删除 key,否则返回 0 表示无法释放锁。 ### 锁的过期机制 通过 `EX` 参数设置的过期时间,可以防止客户端在获取锁后发生崩溃或网络中断而导致锁无法释放的情况。一旦 key 过期,Redis 会自动删除该 key,从而释放锁。这种机制为分布式锁提供了额外的安全性保障 [^2]。 ### 总结 Redis 中的 `SET NX EX` 命令通过原子性操作确保了分布式锁的设置和过期时间不会被中断,从而避免了多个客户端同时获取锁的风险。结合 Lua 脚本,可以进一步实现锁的安全释放,确保只有持有锁的客户端能够释放锁。此外,过期时间的设置也防止了锁的永久占用,从而提高了系统的可靠性和容错能力 。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值