Redis的复合SET命令和简易的分布式锁优化

文章探讨了在不引入额外框架的情况下,如何利用Redis的SET复合命令优化分布式锁,以解决并发执行和异常处理的问题。通过结合SET命令的EX、NX选项,实现了原子性的加锁操作,并给出了具体的Java代码示例。最后提醒在生产环境中应优先考虑使用成熟的分布式锁库,如Redisson。

前提

最近在跟进一个比较老的系统的时候,发现了所有调度任务使用了spring-context里面的@Scheduled注解和自行基于Redis封装的简易分布式锁控制任务不并发执行。为了不引入其他框架的情况下做一些简单优化,笔者花点时间去研读了一下Redis的SET命令的相关文档。

场景还原

使用@Scheduled注解实现定时任务,使用spring-data-redis提供的API实现简易的Redis分布式锁的伪代码如下:

// 每30分钟跑一次
@Scheduled(cron = "* */30 * * * ? ")
public void scheduledMethod(){
	// 判断KEY存在性并且设置KEY,带超时时间5分钟
	if (StringRedisTemplate#opsForValue()#hasKey("定时任务唯一字符串标识")){
		StringRedisTemplate#opsForValue()#set("定时任务唯一字符串标识", "1[这里暂时可以使用任何值]", 5 , TimeUnit.MINUTES);
	}
	// 这里做调度正常业务逻辑
	doBusiness();
	// 删除KEY
	StringRedisTemplate#opsForValue()#delete("定时任务唯一字符串标识");
}

上面的代码存在如下显然的缺陷:

  1. 如果应用部署多个节点,由于判断KEY的存在性和SET操作是两个操作(非原子操作),该定时任务有可能在同一个时刻并发执行多次。
  2. 如果业务逻辑执行方法doBusiness()抛出了异常,会导致删除KEY的操作无法执行,KEY会到达超时时间后被删除,这个时候相当于加锁时间长达5分钟,显然是无法接受的。

但是实际上,以上两个问题在生产环境中并没有出现过,分析一下具体原因是:

  • 对于第1点,该应用在生产环境只部署了2个节点,节点的重启时间并不相同,所以从天然上避免了重复执行的问题,如果CRON表达式设计
### Redis 实现分布式锁命令 Redis 以其高性能简单易用的特性,广泛应用于分布式锁实现实现分布式锁时,关键在于确保操作的原子性,以避免多个客户端同时获取锁导致的冲突问题。以下是 Redis 中常用的命令及其使用方式,用于实现分布式锁。 #### 1. `SET` 命令 `SET` 命令实现分布式锁的核心命令之一,特别是在 Redis 2.6.12 及以上版本中引入了 `SET` 命令的扩展参数,可以同时设置键值、过期时间是否仅当键不存在时设置。这些参数使得 `SET` 成为实现分布式锁的理想选择。 ```redis SET key value NX PX milliseconds ``` - `key`:锁的标识符。 - `value`:通常设置为唯一标识符(如客户端ID),用于后续解锁时校验。 - `NX`:仅当键不存在时设置成功。 - `PX`:设置键的过期时间(毫秒)。 通过 `NX` 参数,可以确保多个客户端不会同时获取同一把锁。而通过 `PX` 参数,可以为锁设置一个合理的过期时间,以避免死锁问题。 #### 2. `DEL` 命令 `DEL` 命令用于释放分布式锁。在释放锁之前,需要确保当前客户端是锁的持有者,因此通常需要结合 Lua 脚本实现原子性操作。 ```redis EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 key value ``` - `KEYS[1]`:锁的键。 - `ARGV[1]`:锁的值(客户端唯一标识)。 通过 Lua 脚本,可以确保检查锁删除锁的操作是原子的,从而避免在检查锁删除锁之间其他客户端获取锁而导致的错误释放。 #### 3. `EXPIRE` `PEXPIRE` 命令 在某些情况下,可能需要动态延长锁的过期时间。例如,当锁的持有者仍在处理任务时,可以通过 `EXPIRE` 或 `PEXPIRE` 命令为锁续期。 ```redis EXPIRE key seconds PEXPIRE key milliseconds ``` - `key`:锁的键。 - `seconds` 或 `milliseconds`:新的过期时间。 需要注意的是,续期操作也需要确保当前客户端是锁的持有者,因此通常也需要结合 Lua 脚本实现。 #### 4. `GET` 命令 `GET` 命令用于获取锁的值,以验证当前客户端是否是锁的持有者。例如,在续期或释放锁之前,可以通过 `GET` 获取锁的值,并与客户端的唯一标识进行比较。 ```redis GET key ``` - `key`:锁的键。 #### 5. Lua 脚本 Lua 脚本是 Redis实现原子操作的重要工具。在分布式锁实现中,Lua 脚本通常用于确保多个操作的原子性,例如检查锁并删除锁、检查锁并续期等。 ```redis EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end" 1 key value seconds ``` - `KEYS[1]`:锁的键。 - `ARGV[1]`:锁的值(客户端唯一标识)。 - `ARGV[2]`:新的过期时间(秒)。 通过 Lua 脚本,可以确保检查锁、续期锁的操作是原子的,从而避免竞态条件。 ### 相关问题 1. Redis 分布式锁实现原理是什么? 2. 如何确保 Redis 分布式锁的可靠性? 3. Redis 分布式锁在高并发场景下的性能如何优化? 4. Redis 分布式锁的自动续期机制是如何实现的? 5. Redis 分布式锁与 ZooKeeper 分布式锁的优缺对比是什么?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值