`

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)) {  永远相等不了

相关推荐

    C#实操控制并发之Lock和Redis分布式锁

    本文将深入探讨C#中如何使用Lock和Redis分布式锁来解决并发问题,以秒杀系统为例进行阐述。 首先,让我们理解什么是并发控制。并发控制是指在多线程环境下确保数据的一致性和完整性,防止多个线程同时访问同一资源...

    java面试题_高并发、高可用、分布式(9题)

    Java并发工具类如Semaphore(信号量)、CountDownLatch和CyclicBarrier也是处理并发问题的重要工具。 2. **高可用性** 高可用性(High Availability, HA)意味着系统能够在组件故障时仍能正常工作。Java中的负载...

    Redis与Zookeeper高并发分布式锁实战.ppt

    redis和zk两种不同方式实现分布式锁,互联网开发小伙伴必备技能!

    记录redisson实现redis分布式事务锁

    Redisson是基于Redis的Java客户端,它提供了丰富的数据结构和服务,包括分布式锁、信号量、队列、计数器等,极大地扩展了Redis在分布式系统中的应用能力。本篇文章将详细探讨如何使用Redisson实现Redis分布式事务锁...

    java高并发相关知识,threadLocal,分布式锁,java各种锁等

    java高并发相关知识,threadLocal,分布式锁,java各种锁等

    redis实现分布式锁,自旋式加锁,lua原子性解锁

    在分布式环境中,由于网络延迟和并发操作,普通的单机锁可能无法满足需求,因此需要引入分布式锁来确保同一时刻只有一个客户端能够获取锁并进行操作。 Redis中的分布式锁实现通常基于`SETNX`命令或`SET`命令的`nx`...

    构建JAVA大型分布式电商项目实战高并发集群分布式系统架构PDF+视频.rar

    本项目实战教程涵盖了高并发、集群以及分布式系统架构等关键知识点,旨在帮助Java架构师提升技能,实现高性能、高可用和可扩展的电商系统。 1. **Java基础与高级特性** - Java的基础语法、面向对象编程、异常处理...

    各种锁汇总,乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁、行级锁等

    本文将深入探讨标题和描述中提及的各种锁,包括乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁以及行级锁。 1. **乐观锁**:乐观锁假设多线程环境中的冲突较少,所以在读取数据时不加锁,只有...

    redisTemplate封装成redisUtils和分布式锁实现

    本篇将深入探讨如何将RedisTemplate封装成RedisUtils工具类,并实现分布式锁功能。 首先,我们需要引入Spring Data Redis的依赖库,这通常在项目的pom.xml或build.gradle文件中完成。添加对应版本的Redis连接器和...

    使用ZooKeeper实现分布式锁

    分布式系统中的并发控制是一个复杂而关键的问题...总之,ZooKeeper的分布式锁机制为解决分布式环境下的并发问题提供了强大支持,特别是在保证订单编号唯一性的场景下,它能够有效地防止数据冲突,保证系统的稳定运行。

    《大型分布式网站架构设计与实践》 Java 并发编程实战

    《大型分布式网站架构设计与实践》是一本深入探讨如何构建高效、可扩展的分布式系统的技术专著,结合了Java并发编程的实际应用。本书旨在帮助读者理解在高流量、大规模应用场景下,如何通过精心设计的架构和Java并发...

    zk使用curator实现分布式锁

    分布式锁是多节点并发操作时防止数据不一致性的关键机制。在传统的单机系统中,我们可以使用synchronized关键字或ReentrantLock等来实现线程同步,但在分布式环境中,我们需要一种跨机器的锁机制,这就是ZooKeeper和...

    003 redis分布式锁 jedis分布式锁 Redisson分布式锁 分段锁

    其中,分布式锁作为解决多线程并发问题的关键技术,被广泛应用于数据一致性、资源独占等场景。本篇文章将详细探讨Redis作为分布式锁的实现方式,包括Jedis和Redisson两个主流的客户端库的使用,以及分段锁的概念和...

    redis分布式锁工具包,提供纯Java方式调用,支持传统Spring工程.zip

    综上所述,这个压缩包提供的Redis分布式锁工具包为Java开发者提供了一种简单、高效的方法来解决分布式环境下的锁问题,特别适合于处理高并发的快应用和企业级应用。通过集成到Spring工程中,开发人员可以利用Redis的...

    java并发编程内部分享PPT

    Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。这份“java并发编程内部分享PPT”显然是一个深入探讨这一主题的资料,旨在帮助开发者...

    redis分布式锁.zip

    Redis 分布式锁是分布式系统中解决并发控制和数据一致性问题的一种常见机制。在大型分布式应用中,单机锁无法满足需求,因为它们局限于单个服务器。Redis 的高可用性和低延迟特性使其成为实现分布式锁的理想选择。...

    (PDF带目录)《Java 并发编程实战》,java并发实战,并发

    《Java 并发编程实战》是一本专注于Java并发编程的权威指南,对于任何希望深入了解Java多线程和并发控制机制的开发者来说,都是不可或缺的参考资料。这本书深入浅出地介绍了如何在Java环境中有效地管理和控制并发...

    redisson实现分布式锁

    Redisson是一款功能丰富的Java客户端,它提供了对Redis服务器的全面支持,包括数据结构服务、分布式服务、锁服务等。在分布式系统中,锁是保证数据一致性的重要工具,而Redisson的分布式锁则为Java开发者提供了一种...

    数据库实现分布式锁.txt

    当多个服务或实例需要同时操作同一份资源时,分布式锁可以保证在任意时刻只有一个服务能获得锁并进行操作,从而避免并发冲突。 #### 二、实现原理 在本案例中,我们将使用数据库作为锁存储介质。具体来说,创建一...

    Java Redis分布式锁的正确实现方式详解

    Java Redis分布式锁是指使用Redis实现的分布式锁机制,旨在解决分布式系统中的并发问题。分布式锁有三种实现方式:数据库乐观锁、基于Redis的分布式锁和基于ZooKeeper的分布式锁。本篇博客将详细介绍第二种方式,...

Global site tag (gtag.js) - Google Analytics