`

java并发(二十二)分布式锁

 
阅读更多
Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。

用SETNX实现分布式锁
利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:
SETNX lock.foo <current Unix time + lock timeout + 1>
  • 如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
  • 如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。


解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。

发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:

C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
C1 发送DEL lock.foo
C1 发送SETNX lock.foo 并且成功了。
C2 发送DEL lock.foo
C2 发送SETNX lock.foo 并且成功了。
这样一来,C1,C2都拿到了锁!问题大了!

幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:

C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,C3通过下面的操作来尝试获得锁:
GETSET lock.foo <current Unix time + lock timeout + 1>
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

示例伪代码
根据上面的代码,我写了一小段Fake代码来描述使用分布式锁的全过程:
# get lock
lock = 0
while lock != 1:
    timestamp = current Unix time + lock timeout + 1
    lock = SETNX lock.foo timestamp
    if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
        break;
    else:
        sleep(10ms)

# do your job
do_job()

# release
if now() < GET lock.foo:
    DEL lock.foo
是的,要想这段逻辑可以重用,使用python的你马上就想到了Decorator,而用Java的你是不是也想到了那谁?AOP + annotation?行,怎样舒服怎样用吧,别重复代码就行。

java之jedis实现
expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会
    /**
     * Acquire lock.
     * 
     * @param jedis
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException
     *             in case of thread interruption
     */
    public synchronized boolean acquire(Jedis jedis) throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); //锁到期时间

            if (jedis.setnx(lockKey, expiresStr) == 1) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = jedis.get(lockKey); //redis里的时间
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
                // lock is expired

                String oldValueStr = jedis.getSet(lockKey, expiresStr);
                //获取上一个锁到期时间,并设置现在的锁到期时间,
                //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= 100;
            Thread.sleep(100);
        }
        return false;
    }


一般用法
其中很多繁琐的边缘代码
包括:异常处理,释放资源等等
        JedisPool pool;
        JedisLock jedisLock = new JedisLock(pool.getResource(), lockKey, timeoutMsecs, expireMsecs);
        try {
            if (jedisLock.acquire()) { // 启用锁
                //执行业务逻辑
            } else {
                logger.info("The time wait for lock more than [{}] ms ", timeoutMsecs);
            }
        } catch (Throwable t) {
            // 分布式锁异常
            logger.warn(t.getMessage(), t);
        } finally {
            if (jedisLock != null) {
                try {
                    jedisLock.release();// 则解锁
                } catch (Exception e) {
                }
            }
            if (jedis != null) {
                try {
                    pool.returnResource(jedis);// 还到连接池里
                } catch (Exception e) {
                }
            }
        }

犀利用法
用匿名类来实现,代码非常简洁
至于SimpleLock的实现,请在我附件中下载查看
        SimpleLock lock = new SimpleLock(key);
        lock.wrap(new Runnable() {
            @Override
            public void run() {
                //此处代码是锁上的
            }
        });



20180330
和同事一起吃饭,聊到释放锁的时候,要是同一个线程。所以这一部分在获取锁的时候,要增加token,如果不是同一个线程,不允许释放锁。

附件是分布式锁的完整实现和用法,有需要交流的朋友,可以随时留言。
分享到:
评论
30 楼 beyondfengyu 2016-11-30  
beyondfengyu 写道
如果每个客户进程的时间不同步,时间超前的进程是不是更容易得到锁呢?
 String currentValueStr = jedis.get(lockKey); //redis里的时间  
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

时间超前的进程在执行到这里时,会强制夺取原来进程的锁,因为在它看来这个key已经失效,如果这个进程刚好执行的是耗时任务,那么其它的进程都可能很难拿到它的锁

楼主有没有想过这种问题?


如果这个时间超前的进程拿到锁后,刚好执行耗时任务,因为别的进程在判断锁是否超时时,由于它们的时间远低于超前的进程,所以总是失败,这样导致耗时进程一直占用这个锁。
29 楼 beyondfengyu 2016-11-30  
如果每个客户进程的时间不同步,时间超前的进程是不是更容易得到锁呢?
 String currentValueStr = jedis.get(lockKey); //redis里的时间  
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

时间超前的进程在执行到这里时,会强制夺取原来进程的锁,因为在它看来这个key已经失效,如果这个进程刚好执行的是耗时任务,那么其它的进程都可能很难拿到它的锁

楼主有没有想过这种问题?
28 楼 随WW便 2016-11-23  
[i][b]
[flash=200,200][url][img][img][img][list]
[*][list]
[*][*][list]
[*][*][*][list]
[*][*][*][*]
引用
引用
引用
引用
[*][*][*][/list]
[*][*][/list]
[*][/list]
[/list][/img][/img][/img][/url][/flash]
[/b][/i]
27 楼 binghc 2016-09-23  
把 26 - 34 行替换成
if (jedis.del(lockKey) == 1) {
    continue;
}
即使多台客户单完全同步,也不可能发生多个客户端拿到锁的情况了吧
26 楼 binghc 2016-09-23  
你们有没有想过,如果这些客户端本地时间不是同步的,甚至可能上下浮动好几分钟,会发生什么事情
25 楼 clean1981 2016-07-13  
为什么不在del的时候判断他是否删除成功呢?如果这样判断是否会阻止C1,C2同时拿到锁呢?
24 楼 wenj91 2016-06-26  
最近特别需要分布式锁这方面的知识,请教下该如何学习这方面的知识呢?
23 楼 jtyb 2016-05-18  
应该用setnxpx改写一下了,参考官网的实现原理,会简单很多
22 楼 m635674608 2016-05-06  
85977328 写道
85977328 写道
引用
JAVA实现的失效的情况是,两台完全同步的机器,每步代码执行的速度都一样,有可能让多个节点同时拿到锁。

精辟,如果完全相同,完全同步,不会有问题
第21行 String currentValueStr = jedis.get(lockKey); //redis里的时间
获取的值完全相同

但是第26行 String oldValueStr = jedis.getSet(lockKey, expiresStr); 
只有一个客户端能获取到超时很大的时间
然后进行下一步的相等判断,进而取得锁

就算第26行set的值完全相等,但是get到的不会完全相当,第一个客户端会拿到currentValueStr相等的值,其他客户端再get到值,已经不是超时的时间了,他们就无法通过第22行 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {的判断
而已经通过第22行判断的代码,currentValueStr是超时的老值,不是新值,所以后面的29行
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {  永远相等不了


我感觉有点问题,当多台服务器同事执行到26行代码的时候,并且 expiresStr值相等的时候,会有多个获得锁?你们觉得了?
21 楼 zxjlwt 2015-11-30  
学习了。
http://surenpi.com
20 楼 li200429 2015-10-08  
这个分布式锁有问题吧?我试了,每次定时任务执行的时候,
if (jedis.setnx(lockKey, expiresStr) == 1) { 
            // lock acquired 
            locked = true; 
            return true; 
        } 
这段代码都执行了,锁没有生效哦~~
19 楼 85977328 2015-08-08  
hj01kkk 写道
setnx原子操作,accquire和release时需要用synchronized加锁吗?若在这两个方法里先用setnx后,根据返回值判断走不同分支,这样是不是可以避免在这里加悲观锁了?

一样的,锁的处理有两种
1)等待
2)快速失效
看具体业务逻辑了,我这种锁的用法是等待。
18 楼 hj01kkk 2015-07-27  
setnx原子操作,accquire和release时需要用synchronized加锁吗?若在这两个方法里先用setnx后,根据返回值判断走不同分支,这样是不是可以避免在这里加悲观锁了?
17 楼 lp1137917045 2015-07-21  
受益匪浅,但是:
如果一个持有锁的客户端失败或崩溃了不能释放锁  是怎么理解的,我认为,如果给锁设置了合适的过期时间,并且在finally中释放锁,那么完全不用考虑死锁的问题。是有那种情况会导致不会即使的执行finally中的方法吗? 另外,syncronized 在分布式集群环境中就不起作用了吗? 小弟工作经验尚浅,忘楼主不吝赐教
16 楼 85977328 2015-05-04  
demoxshiroki 写道
楼主这种写法CAS操作的redis化,实现了非公平锁, 可否想过公平锁的实现。中间节点放弃的时候锁的处理

分布式公平锁倒没想到过
回头我试试
15 楼 demoxshiroki 2015-04-27  
楼主这种写法CAS操作的redis化,实现了非公平锁, 可否想过公平锁的实现。中间节点放弃的时候锁的处理
14 楼 85977328 2014-09-27  
可以在执行过程中,延长分布式锁的超时时间。分布式锁本质上就是个KEY的超时时间
13 楼 waixin 2014-09-25  
如果是当前A获取到了lock, 但由于执行时间过长,导致超时了!
这个时候lock可能被别的拿到,但在A释放的时候并不能确定del的lock就是它的了。

这种情况有没有好的解决方案(除了设置超长超时时间)?
12 楼 zhuyx808 2014-05-25  
请问锁的嵌套问题怎么解决?

a方法需要锁定执行,b方法也需要锁定执行,而a方法里面调用b方法了……

伪代码:

a(){
lock(){
.....
b();
......
}

}

b(){
lock(){
......

}
}

--------------------------------------------------------

首先a方法拿到锁了,执行a方法的逻辑,执行到调用b方法,b方法试图去拿锁,但b方法拿不到锁,于是b方法就等待了,b方法等待的结果是a也执行不动了,于是都挂了
11 楼 85977328 2014-03-20  
85977328 写道
引用
JAVA实现的失效的情况是,两台完全同步的机器,每步代码执行的速度都一样,有可能让多个节点同时拿到锁。

精辟,如果完全相同,完全同步,不会有问题
第21行 String currentValueStr = jedis.get(lockKey); //redis里的时间
获取的值完全相同

但是第26行 String oldValueStr = jedis.getSet(lockKey, expiresStr); 
只有一个客户端能获取到超时很大的时间
然后进行下一步的相等判断,进而取得锁

就算第26行set的值完全相等,但是get到的不会完全相当,第一个客户端会拿到currentValueStr相等的值,其他客户端再get到值,已经不是超时的时间了,他们就无法通过第22行 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {的判断
而已经通过第22行判断的代码,currentValueStr是超时的老值,不是新值,所以后面的29行
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {  永远相等不了

相关推荐

Global site tag (gtag.js) - Google Analytics