`
85977328
  • 浏览: 1899379 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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,如果不是同一个线程,不允许释放锁。

附件是分布式锁的完整实现和用法,有需要交流的朋友,可以随时留言。
分享到:
评论
10 楼 85977328 2014-03-20  
引用
JAVA实现的失效的情况是,两台完全同步的机器,每步代码执行的速度都一样,有可能让多个节点同时拿到锁。

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

但是第26行 String oldValueStr = jedis.getSet(lockKey, expiresStr); 
只有一个客户端能获取到超时很大的时间
然后进行下一步的相等判断,进而取得锁
9 楼 gaddma 2014-03-20  
学习了很不错

因为JAVA的实现和伪代码的实现有所不同,所以仔细对比了一下
伪代码是通过  当前时间  和  getset返回的库里面的时间对比  判断谁应该拿到锁。
JAVA实现是通过 对比两次get 和 getset的返回值的不同,来判断谁应该拿到锁。

两种实现在大多数情况都不会出问题,但是也有失败的情况
1,伪代码失效的情况是 设置超时时间和get的网络开销不相上下的时候。这样容易让一行三次获取数据最后导致成功,很容易让锁失效,替代前一个锁,不过这不算问题,因为设置过小的锁超时时间本身就是程序bug,而且就算这种情况也可以保证每个获取锁的节点依次的进入锁。

2,JAVA实现的失效的情况是,两台完全同步的机器,每步代码执行的速度都一样,有可能让多个节点同时拿到锁。

个人比较赞同伪代码中的实现。
原因是伪代码 如果超时时间设置合理的话,后续的节点最多会把超时时间延迟个几个毫秒,但是后续的节点都不会拿到锁。

建议修改~
8 楼 85977328 2014-03-13  
xugangqiang 写道
85977328 写道
xugangqiang 写道
牛人们,在写这么NB的代码前,
能不能简单的说下使用的场景?
即为什么需要使用分布式锁?

现在的应用,都是无状态应用
多台服务器,想建立同步块的时候
就得分布式锁

为什么多台服务器需要建立同步块?
有没有不需要分布式锁的实现?

有啊
只有及个别案例,需要分布式锁
根据CAP理论
强一致性这种
执行一种操作,要求阻塞全部服务器集群的应用
比如缓的失效中心等等
7 楼 xugangqiang 2014-03-13  
85977328 写道
xugangqiang 写道
牛人们,在写这么NB的代码前,
能不能简单的说下使用的场景?
即为什么需要使用分布式锁?

现在的应用,都是无状态应用
多台服务器,想建立同步块的时候
就得分布式锁

为什么多台服务器需要建立同步块?
有没有不需要分布式锁的实现?
6 楼 85977328 2014-03-13  
xugangqiang 写道
牛人们,在写这么NB的代码前,
能不能简单的说下使用的场景?
即为什么需要使用分布式锁?

现在的应用,都是无状态应用
多台服务器,想建立同步块的时候
就得分布式锁
5 楼 xugangqiang 2014-03-13  
牛人们,在写这么NB的代码前,
能不能简单的说下使用的场景?
即为什么需要使用分布式锁?
4 楼 85977328 2014-03-13  
gaddma 写道
nice , 学习了

是慕童吗?
3 楼 gaddma 2014-03-13  
nice , 学习了
2 楼 85977328 2014-03-13  
jianfeihit 写道
写的不错,学习了

杨总没事多来逛逛~
1 楼 jianfeihit 2014-03-13  
写的不错,学习了

相关推荐

    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