场景应用:Redis使用setnx命令实现分布式锁

何时需要分布式锁?

在分布式的环境下,当多个server并发修改同一个资源时,为了避免竞争就需要使用分布式锁。

那为什么不能使用Java自带的锁呢?因为Java中的锁是面向多线程设计的,它只局限于当前的JRE环境。而多个
server实际上是多进程,是不同的JRE环境,所以Java自带的锁机制在这个场景下是无效的

如何实现分布式锁?

探究单节点锁:setnx命令

采用Redis实现分布式锁,就是在Redis里存一份代表锁的数据,通常用字符串即可。实现分布式锁的思路,以及优化的过程如下:

1. 加锁:

版本一:但是这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。

setnx key value

版本二:给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。

setnx key value 
expire key seconds

版本三(推荐):通过“set...nx...”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。

set key value nx ex seconds

PS:关于setnx与set…nx的探究请参考文件末尾的setnx的学习

2. 解锁:

解锁就是删除代表锁的那份数据。

del key

带来的问题

看起来已经很完美了,但实际上还有隐患,如下图所示例子:进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,它此时释放的可能是其他线程的锁。

在这里插入图片描述

想要解决这个问题,我们需要解决两件事情:

  1. 在加锁时就要给锁设置一个标识,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key赋一个随机值,来充当进程的标识。
  2. 解锁时要先判断、再释放,这两步需要保证原子性,否则第二步失败的话,就会出现死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。

按照以上思路,优化后的命令如下:

# 加锁 
set key random-value nx ex seconds 

# 解锁 
if redis.call("get",KEYS[1]) == ARGV[1] then 
	return redis.call("del",KEYS[1]) 
else
	return 0 
end

然后使用eval 用来执行 lua 脚本就OK了。

上述分布式锁的实现方案,是建立在单个主节点之上的。它的潜在问题如下图所示,如果进程A在主节点上加锁成功,然后这个主节点宕机了,则从节点将会晋升为主节点。若此时进程B在新的主节点上加锁成果,之后原主节点重启,成为了从节点,系统中将同时出现两把锁,这是违背锁的唯一性原则的。

在这里插入图片描述

探究分布式锁:RedLock算法

总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的。若要保证分布式锁的高可用,则可以采用多个节点的实现方案。这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案。

该算法基于多个Redis节点,它的基本逻辑如下:

  • 这些节点相互独立,不存在主从复制或者集群协调机制;
  • 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;
  • 解锁:向所有的实例发送DEL命令,进行解锁;

RedLock算法的示意图如下,我们可以自己实现该算法,也可以直接使用Redisson框架。
在这里插入图片描述

补充:setnx命令学习

setnx是set if not exists 的缩写,用来设置键的值

setnx命令返回整数值,当返回1时表示设置值成果,当返回0时表示设置值失败(key已存在)

一般我们不建议直接使用setnx命令来实现分布式锁,因为为了避免出现死锁,我们要给锁设置一个自动过期时间。而setnx命令和设置过期时间的命令不是原子的,可能加锁成功而设置过期时间失败,依然存在死锁的隐患。对于这种情况,Redis改进了set命令,给它增加了nx选项,启用该选项时set命令的效果就会setnx一样了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RedisSETNX命令用于在键不存在时设置键值对,如果键已经存在,则不做任何操作。利用SETNX命令可以实现简单的分布式锁。以下是一个使用SETNX实现分布式锁的示例代码: ```python import redis import time def acquire_lock(lock_name, acquire_timeout=10, lock_timeout=10): # 创建redis连接 redis_conn = redis.Redis() # 生成唯一的锁标识 lock_key = f"lock:{lock_name}" # 获取当前时间戳,用于计算锁超时时间 timestamp = int(time.time()) + acquire_timeout # 循环尝试获取锁 while int(time.time()) < timestamp: # 尝试获取锁 if redis_conn.setnx(lock_key, "locked"): # 设置锁超时时间 redis_conn.expire(lock_key, lock_timeout) return True # 短暂休眠,避免频繁尝试获取锁 time.sleep(0.1) return False def release_lock(lock_name): # 创建redis连接 redis_conn = redis.Redis() # 生成唯一的锁标识 lock_key = f"lock:{lock_name}" # 删除锁 redis_conn.delete(lock_key) ``` 在上述示例代码中,`acquire_lock`函数用于获取分布式锁,`release_lock`函数用于释放分布式锁。具体实现过程如下: 1. 创建Redis连接。 2. 生成唯一的锁标识,一般以`lock:`为前缀加上具体的锁名。 3. 计算获取锁的超时时间戳,即当前时间戳加上获取锁的超时时间。 4. 循环尝试获取锁,如果成功获取到锁,则设置锁的超时时间,并返回True;如果超过超时时间仍未获取到锁,则返回False。 5. 释放锁的过程比较简单,直接删除对应的锁标识即可。 需要注意的是,分布式锁实现还需要考虑异常情况下的处理、防止锁被误释放等问题,上述代码仅作为示例,具体应用场景中可能需要根据实际需求进行适当修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值