摘要:在分布式环境中,当需要对共享资源进行互斥访问时就需要使用分布式锁,分布式锁就是一种用于分布式环境下的、保证共享资源被不同进程互斥访问的技术手段。 由于redis的单线程特性,以及提供setnx、getset的命令,可以很方便地实现分布式锁。
什么是分布式锁呢?什么时候需要用分布式锁呢?
在分布式环境中,当需要对共享资源进行互斥访问时就需要使用分布式锁,分布式锁就是一种用于分布式环境下的、保证共享资源被不同进程互斥访问的技术手段。
由于redis的单线程特性,以及提供setnx、getset的命令,可以很方便地实现分布式锁。

首先让我们想想,一个高效的分布式锁需要什么条件呢?
1、安全属性:互斥,无论什么时候,只有一个客户端能够获得锁;
2、容错属性:最终都会有一个客户端能够获得锁,即使持有锁的客户端宕机或者挂起;

我们知道,redis的setnx命令在存储一个key的值时,当key不存在时存储最新的值并返回1,当key存在时保留原来的值返回0。所以使用setnx命令可以很容易构造一种分布式锁的方案:
1、setnx key_lock value  判断返回值是否为1
  •      如果为1,表示该客户端获得锁,则执行下一步任务;
  •      如果为0,表示未能获得锁,则等待或者继续使用setnx命令请求锁;

2、del key_lock 释放锁
     客户端获得锁后,执行完任务后,需要释放锁,使用del命令直接把锁的key删除即可;

上面的方案已经实现了获取锁、等待锁、释放锁的方法,这就是用redis实现一个分布式锁的基础方案了。但是这个方案是不是一个完美的方案了呢?不是的,死锁的问题还没有解决。

场景一:
     如果客户端A获取锁后,执行任务过程中由于异常突然崩溃了,那么客户端A就不能自己释放锁。

所以,不能简单地使用del命令来释放锁,我们需要有其它的办法来释放锁,即让锁能够自动失效。使用setnx key_lock value命令获取锁时,我们可以设置值为当前的时间戳+失效时间,当另一个客户端请求锁失败时,判断当前的key_lock的值是否大于当前的时间戳,若大于说明锁已经失效,可以被重新使用,然后调用del命令删除锁key_lock,再调用setnx命令重新获取锁;若小于说明锁还没有失效,则继续等待。这样的话就能够保证已获取锁的客户端由于崩溃不能自己释放锁时,锁依然能够被重新使用,避免死锁的出现。

上面的方案可以解决死锁的问题了,但在并发的情况下,如果多个客户端检测到锁超时后都去释放原来的锁,那么有可能导致多个客户端同时获得锁。

场景二:
  •      假设客户端A获得锁key_lock后执行任务时间过长导致锁已经超时,此时客户端A还持有锁;
  •      某一时刻客户端B和客户端C都请求获取锁,失败后,调用get命令获取key_lock的值,判断发现锁已经超时;     
  •      客户端B发送del命令删除key_lock,再调用【setnx key_lock 当前时间戳+失效时间】命令获取锁,返回为1说明成功了;
  •      客户端C发送del命令删除key_lock,再调用【setnx key_lock 当前时间戳+失效时间】命令获取锁,返回为1说明也成功了;
    
此时,客户端B和客户端C都同时持有锁key_lock了,这就有问题了!!!要想避免这种问题,还需要优化上面的方案,在判断锁超时后需要用到redis的getset命令来获取锁。

场景三:
  •      假设客户端A获得锁key_lock后执行任务时间过长导致锁已经超时,此时客户端A还持有锁;
  •      某一时刻客户端B和客户端C都请求获取锁,失败后,调用【get key_lock】获取锁的值,判断发现锁已经超时;
  •      客户端B调用【getset key_lock  当前时间戳+失效时间】,如果返回值小于当前的时间(还是超时),说明已经获取锁了;
  •      客户端C调用【getset key_lock  当前时间戳+失效时间】,如果返回值大于当前的时间,说明锁已经被B获取了,那么C获取锁失败;

上面的方案可能还个小问题,C调用getset命令会重新设置key_lock的值,但由于B和C调用相关的时间比较短,就算C改写了B设置的时间,相差的值也不会太大,可以忽略不计。

释放锁时如果仅仅是调用del命令来删除,那么在锁超时的情况下,持有超时锁的客户端有可能删除现持有锁客户端的锁。

场景四:
  •      假设客户端A获得锁key_lock后执行任务时间过长导致锁已经超时,此时客户端A还持有锁;
  •      某一时刻客户端B请求获取锁,失败后,调用【get key_lock】获取锁的值,判断发现锁已经超时;
  •      客户端B调用【getset key_lock  当前时间戳+失效时间】,如果返回值小于当前的时间(还是超时),说明已经获取锁了;
  •      客户端A执行完任务后,调用del key_lock释放锁;

可以看到,此时A释放的锁已经不是它自己的了,而是B获取的锁,这样A会把B的锁释放了。
为了避免上面的问题,应该在释放时判断一下key_lock的值,如果是自己设置的值,那么调用del命令释放,如果不是直接忽略。

Java实现获取锁的代码:
public synchronized boolean lock(Jedis jedis){
    int retryTime = RETRY_TIME;
    try {
        while (retryTime > 0) {
            //锁到期时间
            lockValue = System.currentTimeMillis() + EXPIRE_TIME + 1;
            String lockValueStr = String.valueOf(lockValue);
            //判断能否获取锁
            if (jedis.setnx(LOCK_KEY, lockValueStr) == 1) {
                //成功获取锁
                locked = true;
                return locked;
            }
            String currLockVal = jedis.get(LOCK_KEY);
            // 判断锁是否已经失效
            if (currLockVal != null && Long.valueOf(currLockVal) <
            System.currentTimeMillis()) {
                //锁已经失效,使用命令getset设置最新的过期时间
                String oldLockVal = jedis.getSet(LOCK_KEY, lockValueStr);
                //判断锁是否已经被抢占
                if (oldLockVal != null && oldLockVal.equals(currLockVal)) {
                    locked = true;
                    return locked;
                }
            }
            retryTime -= 100;
            Thread.sleep(100);
        }
    }catch (Exception e){
        e.printStackTrace();
    }
    return false;
}

Java实现释放锁的代码:
public synchronized void unlock(Jedis jedis){
    if(locked) {
        String currLockVal = jedis.get(LOCK_KEY);
        if(currLockVal != null &&
            Long.valueOf(currLockVal) == lockValue) {
            jedis.del(LOCK_KEY);
            locked = false;
        }
    }
}

上面的实现方案是基于单Redis实例实现的,可以算是一个比较成熟的分布式锁解决方案,由于key_lock的值各客户端的本地时间戳,各客户端的时间戳有可能不一致,时间戳超前的客户端有可能有更多的机会来抢占超时的锁,也可能抢占未超时的锁。

场景五:
     假设客户端B的时间要比客户端A的要快200个单位时间;
     客户端A此时时间戳为100,setnx key_lock 100+100,返回1获取锁;
     客户端A执行任务,50单位时间后,此时客户端B的时间戳为350,A的时间戳为150;
     客户端B请求锁,由于锁被A持有,返回失败;
     客户端B调用【get key_lock】获取锁的值为200,B的时间戳大于锁的有效值,判断锁失效;
     客户端B调用【getset key_lock  当前时间戳+失效时间】,返回值150小于当前B的时间350,说明B获取锁;
     
我们可以看到,在B判断锁失效时,其实锁对于A来说还是在有效期间的,但由于B的时间比A超前,所以B抢占了A的锁。如果要避免这种问题,需要保证各客户端的时间一致,起码不要相差太大。使用时应该设置锁的超时时间尽可能大,保证任务都能在锁的有效期间完成,在一定程度上设置较大的超时时间也能够防止客户端时间不一致造成的锁被抢占的问题。在下一篇文章《Redis实现分布式锁方案二(Java源码)》里将介绍另外一种用Redis实现分布式锁的方案,它将解决方案一的所有问题。


源码已经提交在GitHub:https://github.com/beyondfengyu/DistributedLock (RedLock.java)


版权说明:如无特殊说明,文章均为本站原创,如需转载请注明出处

本文标题:Redis实现分布式锁方案一(Java源码)

本文地址:http://www.wolfbe.com/detail/201612/384.html

本文标签: 分布式锁 redis java

相关文章

感谢您的支持,朗度云将继续前行

扫码打赏,金额随意

温馨提醒:打赏一旦完成,金额无法退还,请谨慎操作!

扫二维码 我要反馈 回到顶部