redis分布式锁的实现

场景引入

当我们在分布式应用基础上进行开发时,经常会遇到并发问题。比如一个操作去修改用户的状态,第一步先要读取用户的状态,在内存里修改,改完然后再存回去。如果这两个操作同时进行,就会出现并发问题。因为‘读取’和‘保存状态’这两个操作不是原子操作。(原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就会一直运行到结束。中间不会有任何的线程切换。)

分布式锁

分布式锁本质上要实现的目标就是在让redis在内存中占一个“萝卜坑”,当别的进程想要来占位时,发现已经被占了,别的进程只好放弃或者稍后再试。

占坑操作一般用setnx(set if not exists)指令,只允许被一个客户端占领。先来先占领,用完了,再del释放掉。

127.0.0.1:6379> setnx lock:coder true
(integer) 1
do something...
127.0.0.1:6379> del lock:coder
(integer) 1
127.0.0.1:6379> 

如果在do something的时候,出现异常,会导致del命令得不到调用,此时就会陷入死锁,被加的锁永远得不到释放。
于是我们可以在拿到锁之后,再给锁加上一个过期时间,比如10s,这样可以保证中间出现异常,锁也能在规定的时间内释放掉,不会形成死锁。

127.0.0.1:6379> setnx lock:coder true
(integer) 1
127.0.0.1:6379> expire lock:coder 5
(integer) 1
暂停时间超过5秒
127.0.0.1:6379> del lock:coder
(integer) 0
此时锁已经释放掉,del的返回值是0

127.0.0.1:6379> setnx lock:coder true
(integer) 1
127.0.0.1:6379> expire lock:coder 5
(integer) 1
暂停时间不超过5秒
127.0.0.1:6379> del lock:coder
(integer) 1
此时锁还未释放掉,del的返回值是1
127.0.0.1:6379> 

但是还有一个问题,在setnx和expire命令之间,服务器的进程挂掉,人为或者非人为的,就会导致对锁的expire命令得不到执行。也会造成死锁。
究其根本原因还是因为setnx和expire命令不是原子操作,如果能确保这两个命令能够一起执行,就不会出现死锁的问题。
为了解决这个问题,在后期的redis版本中为set方法加入了扩展参数,使得setnx和expire能够一起执行。

set key value [expiration EX seconds|PX milliseconds] [NX|XX]

超时问题

redis的分布式锁是不能够解决超时问题的,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题,因为这时候第一个线程持有的锁过期了,业务逻辑还没执行完,而此时第二个线程就提前持有了这把锁,导致代码不能够严格的串行执行。

不难发现正常情况下锁操作完后都会被手动释放,常见的解决方案是调大锁的超时时间,之后若再出现超时带来的并发问题,人工介入修正数据。这也不是一个完美的方案,因为但业务逻辑执行时间是不可控的,所以还是可能出现超时,当前线程的逻辑没有执行完,其它线程乘虚而入。并且如果锁超时时间设置过长,当持有锁的客户端宕机,释放锁就得依靠redis的超时时间,这将导致业务在一个超时时间周期内不可用。

基本上,如果在执行计算期间发现锁快要超时了,客户端可以给redis服务实例发送一个Lua脚本让redis服务端延长锁的时间,只要这个锁的key还存在而且值还等于客户端设置的那个值。 客户端应当只有在失效时间内无法延长锁时再去重新获取锁(基本上这个和获取锁的算法是差不多的)。

启动另外一个线程去检查的问题,这个key是否超时,在某个时间还没释放。
当锁超时时间快到期且逻辑未执行完,延长锁超时时间的伪代码:

if redis.call("get",KEYS[1]) == ARGV[1] then
          redis.call("set",KEYS[1],ex=3000)  
else
          getDLock();//重新获取锁  

这里提到对线程已有的锁再次请求加锁延长锁时间,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。

集群中的分布式锁

当在集群环境下,上面的分布式锁是有部分缺陷的。
在集群环境中,会存在主节点,从节点之分,当主节点挂掉之后,从节点会取而代之,但是节点之间的切换对于客户端没有任何感知,此时,第一个客户端在主节点申请成功一把锁,但是还未来的及同步到从节点上,主节点就挂掉了,然后未同步到锁的子节点晋升成主节点,但是该节点并没有第一个客户端申请的锁,此时第二个客户端请求相同的锁,会申请成功(理论上是不应该申请成功的),这样就会导致系统中,两个客户端持有同一把锁的情况,不安全性由此产生。
不过这种不安全也仅在主从发生failover的情况下才会产生,而且持续的时间极短,业务系统一般都能够容忍。

redlock算法

为了解决这个问题,发明了redlock算法,同很多分布式算法一样,redlock也使用“大多数机制”。
加锁时,他会向过半的节点发送set key value [expiration EX seconds|PX milliseconds] [NX|XX] 指令,只要
过半节点set成功,就认为加锁成功。释放锁时,需要向所有节点发送del指令。不过redlock算法还需要考虑出错重试等问题,同时因为redlock需要向多个节点进行读写,意味着其相比单实例redis的性能会下降一些。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值