redis分布式锁

redis锁(单例)
目前redis实现分布式锁大致有以下几种方案:

  • 方案一

伪代码:
    

lock(key,requestId){
    if (setnx(key,requestId)==1){
        setexpire(key,5000);
        return true;
    }
        return false;
    }

    unlock(key,requestId){
         String oldVlue = get(key);
         if (requestId.equals(requestId)){
                        del(key);
         }
    }

分析

获取锁方法是通过setnx命令设置k-v,设置成功之后,再设置超时时间,避免因系统问题没有主动释放锁而产生死锁。
解锁是先判断当前的rquestId(客户的的标识)是否和redis里的requestId是否相等,如果相等则删除key,解锁成功,如果
不相等,那么解锁失败,这么做是希望每个客户的只能解锁自己加的锁,避免B删除A的锁,比如A先获取了锁,因为某些原因
阻塞了,一直到锁超过了过期时间仍然没有执行完,这时线程B就会获取锁成功,这时A阻塞结束,执行完之后会释放锁,这时
如果不判断requestId那么A线程就会释放B的锁,如果B的锁被释放了,那么C就会成功请求到锁。因为A线程的特殊情况导致接下来
的恶性循环,所以要极力避免。
这种方案的缺点也是非常明显的,就是setnx和expire两个命令不是原子操作,如果setnx执行完了之后
出现异常导致expire命令没有执行,即使setnx和expire两个操作直接没有任何代码,不会出现异常,
但是如果setnx之后服务进程终止那么还是不会执行expire,因为没有过期时间所以锁会一直存在,死锁就会出现。
不仅如此,解锁的时候也会有问题,因为get和del不是原子操作,所以有可能get获取的值为requestId_1但是在del时redis中的值已经被更成了requestId_2
这样会导致requestId的约束失效,客户端A可能删了客户端B的锁。
虽然概率比较低但是还是有可能出现。

  • 方案二

伪代码:

getLock(String lockKey, int expireTime) {
 
 long expires = System.currentTimeMillis() + expireTime;
 String expiresStr = String.valueOf(expires);
 
 // 如果当前锁不存在,返回加锁成功
 if (setnx(lockKey, expiresStr) == 1) {
  return true;
 }
 
 // 如果锁存在,获取锁的过期时间
 String currentValueStr = get(lockKey);
 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
  // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
  String oldValueStr = getSet(lockKey, expiresStr);
  if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
   // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
   return true;
  }
 }
   
 // 其他情况,一律返回加锁失败
 return false;
 
}

分析

和方案一相比,最大的不同点是,没用通过expire设置过期时间,而是通过把超时时间设置在value中。
这样做的好处是不会像第一种方案因为setnx和expire不是原子操作而出现死锁。
有的文章分析称这种方案没有设置requestId,客户的没有标识,其实这个问题在这种方案中也可以解决,
如下代码,我们可以改写代码expiresStr = requestId + "_"+ String.valueOf(expires);这样做就可以将客户的标识附带到value中
释放锁的时候取出requestId进行判断即可,所以这不是大问题。

我认为比较严重的问题是,这种方案必须要保证所有服务器的时间要一致,如果不一致就会出现问题。
比如客户端A所在的服务器时间A_T要比客户端B所在的服务器时间B_T晚两分钟,A和B同时准备获取锁,
客户端A首先获取锁然后设置失效时间为一分钟后,客户端B获取锁失败然后判断失效时间,因为B所在的服务器时间要比A早两分钟
所以判断为过期,然后重新设置锁的过期时间,获取锁成功。这种情况就很严重,A和B同时获取锁成功,同时执行代码,这时分布式锁没有起到任何作用。

还有一个问题就是如代码中所示getSet方法,可能多个客户端同时做这个操作,虽然接下来做了判断,但是这个判断只能保证只有一个
客户端获取锁,但是getSet操作可能被覆盖。
比如客户端A首先获取锁,但是因为某些原因导致阻塞,此时客户端B和客户端C同时请求锁,发现锁被持有,然后进行下一步判断
锁是否过期,因为A阻塞太久假设已经过期了,那么B和C会都会请求执行getSet方法,但是B和C的服务器的时间不一致,
假如B先执行,B执行getSet(k,B_T) 返回A_T(客户端A设置的值),C执行getSet(k,C_T) 返回B_T(客户端B设置的值);
经过判断A_T=oldValueStr所以B获取锁成功,B_T!=oldValueStr所以C获取锁失败,但是此时redis锁对应的过期时间是C设置的,并不是当前持有锁的B设置的。
假如B和C所在服务器的时间一致,那么过期时间会比预期的时间往后推delay=(C执行getSet的时间)-(B执行getSet时间)

  • 方案三

伪代码:
 

 getLock(String lockKey, String requestId, int expireTime) {
 
  String result = set(lockKey, requestId, "NX", "PX", expireTime);
 
  if (OK.equals(result)) {
   return true;
  }
  return false;
 
 }
 
  releaseLock(String lockKey, String requestId) {
 
  String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  Object result = eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
 
  if (RELEASE_SUCCESS.equals(result)) {
   return true;
  }
  return false;
 
 }

分析

与前两种方案不同是,这次无论是加锁还是解锁都是原子操作,所以方案一和方案二中的问题不会出现,
看起来很完美,但是这三种方案都有一个相同的问题就是过期时间应该设置为多少,无法确定。
因为如果设置的时间太短那么有可能任务还没执行完,锁已经失效。如果设置的时间太长,那么如果任务执行完之后没有成功释放锁,那么锁会一直存在很久,导致其他任务无法正常执行。
所以如果要解决这个问题,需要在这个方案的基础上再另外开启一个watch任务,watch任务必须在持有锁的客户端上运行,定时检测锁的持有情况,如果锁还在被持有,说明任务还没有执行完,那么延迟锁的有效期。
如果这样是不是就不需要客户端标识requestId?
我认为不需要,因为客户端标识的作用是为了保证哪个客户端加的锁由哪个客户端释放锁,因为有了watch机制保证任务没执行完之前不会有其他客户端能够成功获取锁,所以从根本上杜绝了这种情况。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值