女朋友也能看懂的Zookeeper分布式锁原理

Hadoop 与 Zookeeper 集群搭建 Hadoop 是一个分布式系统基础架构,由 Apache 基金会开发,旨在处理大规模数据集。Hadoop 分布式文件系统(HDFS):一种分布式文件系统,能够将大文件切分成多个块,并存储在集群中的不同节点上。具有高容错性、高吞吐量等特点,适合大规模数据的存储。例如,在一个大型互联网公司中,每天产生的海量用户行为数据可以存储在 HDFS 中。MapReduce 编程模型:用于大规模数据集的并行计算。通过将计算任务分解为 Map(映射)和 Reduce(归约)两个阶段,实现数据的分布式处理。 阅读详情

介绍

许多场景中,数据一致性是一个比较重要的话题,在单机环境中,我们可以通过Java提供的并发API来解决;而在分布式环境(会遇到网络故障、消息重复、消息丢失等各种问题)下要复杂得多,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景。本文主要探讨如何利用Zookeeper来实现分布式锁,对比了一些其他方案分布式锁的优缺点。至于使用何种,要因自己的业务场景去决定,没有绝对的方案。

分布式锁是什么?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。

实现分布式锁注意点:
  • 锁的可重入性(递归调用不应该被阻塞、避免死锁)
  • 锁的超时(避免死锁、死循环等意外情况)
  • 锁的阻塞(保证原子性等)
  • 锁的特性支持(阻塞锁、可重入锁、公平锁、联锁、信号量、读写锁)
使用分布式锁注意点:
  • 分布式锁的开销(分布式锁一般能不用就不用,有些场景可以用乐观锁代替)
  • 加锁的粒度(控制加锁的粒度,可以优化系统的性能)
  • 加锁的方式

常见的实现分布式锁方案

数据库

基于数据库表唯一索引

最简单的方式就是直接创建一张锁表,当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。给某字段添加唯一性约束,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

但会引入数据库单点、无失效时间、不阻塞、不可重入等问题。

基于数据库排他锁

如果使用的是MySql的InnoDB引擎,在查询语句后面增加for update,数据库会在查询过程中(须通过唯一索引查询)给数据库表增加排他锁,我们可以认为获得排它锁的线程即可获得分布式锁,通过 connection.commit() 操作来释放锁。

会引入数据库单点、不可重入、无法保证一定使用行锁、排他锁,所以有可能长时间不提交导致占用数据库连接等问题。

优缺点
  • 优点:

直接借助数据库,容易理解。

  • 缺点

会引入更多的问题,使整个方案变得越来越复杂

操作数据库需要一定的开销,有一定的性能问题

使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候

缓存

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,目前有很多成熟的缓存产品,包括Redis、memcached、tair等。

基于 redis 的 setnx()、expire() 方法做分布式锁

setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

redis有个命令可以实现setnx 和 expire 同样效果的原子性指令,博主这边解释并未提及到。命令:
set k1 v1 ex 10 nx 。当然为了实现以上博主说的两个命令原子性操作,也可以使用lua脚本完成。

基于 Redlock 做分布式锁

Redlock 是 Redis 的作者 antirez 给出集群模式的 Redis 分布式锁,它基于 N 个完全独立的 Redis 节点(通常情况下 N 可以设置成 5)

基于 redisson 做分布式锁

redisson 是 redis 官方的分布式锁组件

优缺点
  • 优点
    性能好

  • 缺点

实现中需要考虑的因素太多,
通过超时时间来控制锁的失效时间并不是十分的靠谱

Zookeeper

大致思想

每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定节点目录下,生成一个唯一的临时有序节点(zk有自动生成有序节点的功能)。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题

Zookeeper 实现分布式锁的核心原理
1. 排他锁

排他锁,又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。

排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到。

Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下,创建节点能够保证全局唯一性,可以利用Zookeeper这个特性,实现排他锁。

就是读写互斥、写写互斥、读读互斥

实现原理,有3个核心步骤:定义锁、获取锁、释放锁

  • 定义锁

通过Zookeeper上的数据节点来表示一个锁

  • 获取锁

客户端通过调用 create 方法创建表示锁的临时节点。创建成功,认为客户端获得锁。创建失败,认为锁被占用。同时让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况,再次去获取锁。

  • 释放锁

当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除,锁也就释放了。

正常执行完业务逻辑,客户端主动删除自己创建的临时节点。

2. 共享锁

共享锁,又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

共享锁与排他锁的区别在于,加了排他锁之后,数据对象只对当前事务可见,而加了共享锁之后,数据对象对所有事务都可见。

总结,读读共享,读写互斥。都是读请求不锁资源,资源共享。有读有写就锁资源。

实现原理也是3个核心步骤:

  • 定义锁

通过Zookeeper上的数据节点来表示一个锁,是一个类似于 /lockpath/[hostname]-请求类型-序号的临时顺序节点

  • 获取锁

客户端通过调用 create 方法创建表示锁的临时顺序节点。如果是读请求,则创建 /lockpath/[hostname]-R-序号节点,如果是写请求则创建 /lockpath/[hostname]-W-序号节点

判断获得共享锁的逻辑为:

  1. 创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听

  2. 确定自己的节点序号

  3. 对于读请求,如果比发现有比自己序号小的写请求就等待继续获取锁,没有则获取共享锁,操作资源。 对于写请求,如果发现有比自己序号小的读/写请求就等待获取锁,没有则获取锁。

  4. 接收到Watcher通知后,重复步骤1

  • 释放锁

与排他锁逻辑一致。

3. 羊群效应的产生

在实现共享锁的 “判断读写顺序” 的第1个步骤是:创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。这样的话,任何一次客户端移除共享锁之后,Zookeeper将会发送子节点变更的Watcher通知给所有机器,系统中将有大量的 “Watcher通知” 和 “子节点列表获取” 这个操作重复执行,然后所有节点再判断自己是否是序号最小的节点(写请求)或者判断比自己序号小的子节点是否都是读请求(读请求),从而继续等待下一次通知。

然而,这些重复操作很多都是 “无用的”,实际上每个锁竞争者只需要关注序号比自己小的那个节点是否存在即可。

当集群规模比较大时,这些 “无用的” 操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个客户端释放了共享锁,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知–这就是所谓的 “羊群效应“。

改进后的分布式锁实现:

  1. 客户端调用 create 方法创建一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点

  2. 客户端调用 getChildren 方法获取所有已经创建的子节点列表(这里不注册任何Watcher)

  3. 读请求:向比自己序号小的最后一个写请求节点注册Watcher监听,写请求:向比自己序号小的最后一个节点注册Watcher监听 (有无获取锁的逻辑同上)

  4. 等待Watcher监听,继续进入步骤2

用Curator客户端实现分布式锁

Apache Curator是一个Zookeeper的开源客户端,它提供了Zookeeper各种应用场景(Recipe,如共享锁服务、master选举、分布式计数器等)的抽象封装,接下来将利用Curator提供的类来实现分布式锁,Curator提供的跟分布式锁相关的类有5个,分别是:

  1. Shared Reentrant Lock 可重入锁
  2. Shared Lock 共享不可重入锁
  3. Shared Reentrant Read Write Lock 可重入读写锁
  4. Shared Semaphore 信号量
  5. Multi Shared Lock 多锁

关于错误处理:还是强烈推荐使用ConnectionStateListener处理连接状态的改变。当连接LOST时你不再拥有锁。

1. 可重入锁

Shared Reentrant Lock,全局可重入锁,所有客户端都可以请求,同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类 InterProcessMutex 来实现,它的主要方法:

//构造方法
public InterProcessMutex(CuratorFramework client, String path)
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
// 通过acquire获得锁,并提供超时机制:
public void acquire() throws Exception
public boolean acquire(long time, TimeUnit unit) throws Exception
// 撤销锁
public void makeRevocable(RevocationListener<InterProcessMutex> listener)
public void makeRevocable(final RevocationListener<InterProcessMutex> listener, Executor executor)

定义一个 FakeLimitedResource 类来模拟共享资源,该资源一次只能被一个线程使用,直到使用结束,下一个线程才能使用,否则会抛出异常。

public class FakeLimitedResource {
    private final AtomicBoolean inUse = new AtomicBoolean(false);

    // 模拟只能单线程操作的资源
    public void use() throws InterruptedException {
        if (!inUse.compareAndSet(false, true)) {
            // 在正确使用锁的情况下,此异常不可能抛出
            throw new IllegalStateException("Needs to be used by one client at a time");
        }
        try {
            Thread.sleep((long) (100 * Math.random()));
        } finally {
            inUse.set(false);
        }
    }
}

下面的代码将创建 N 个线程来模拟分布式系统中的节点,系统将通过 InterProcessMutex 来控制对资源的同步使用。

每个节点都将发起10次请求,完成 请求锁–访问资源–再次请求锁–释放锁–释放锁 的过程。

客户端通过 acquire 请求锁,通过 release 释放锁,获得几把锁就要释放几把锁。

这个共享资源一次只能被一个线程使用,如果控制同步失败,将抛异常。

public class SharedReentrantLockTest {
    private static final String lockPath = "/testZK/sharedreentrantlock";
    private static final Integer clientNums = 5;
    final static FakeLimitedResource resource = new FakeLimitedResource(); // 共享的资源
    private static CountDownLatch countDownLatch = new CountDownLatch(clientNums);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < clientNums; i++) {
            String clientName = "client#" + i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    CuratorFramework client = ZKUtils.getClient();
                    client.start();
                    Random random = new Random();
                    try {
                        final InterProcessMutex lock = new InterProcessMutex(client, lockPath);
                        // 每个客户端请求10次共享资源
                        for (int j = 0; j < 10; j++) {
                            if (!lock.acquire(10, TimeUnit.SECONDS)) {
                                throw new IllegalStateException(j + ". " + clientName + " 不能得到互斥锁");
                            }
                            try {
                                System.out.println(j + ". " + clientName + " 已获取到互斥锁");
                                resource.use(); // 使用资源
                                if (!lock.acquire(10, TimeUnit.SECONDS)) {
                                    throw new IllegalStateException(j + ". " + clientName + " 不能再次得到互斥锁");
                                }
                                System.out.println(j + ". " + clientName + " 已再次获取到互斥锁");
                                lock.release(); // 申请几次锁就要释放几次锁
                            } finally {
                                System.out.println(j + ". " + clientName + " 释放互斥锁");
                                lock.release(); // 总是在finally中释放
                            }
                            Thread.sleep(random.nextInt(100));
                        }
                    } catch (Throwable e) {
                        System.out.println(e.getMessage());
                    } finally {
                        CloseableUtils.closeQuietly(client);
                        System.out.println(clientName + " 客户端关闭!");
                        countDownLatch.countDown();
                    }
                }
            }).start();
        }
        countDownLatch.await();
        System.out.println("结束!");
    }
}

控制台打印日志,可以看到对资源的同步访问控制成功,并且锁是可重入的

0. client#3 已获取到互斥锁
0. client#3 已再次获取到互斥锁
0. client#3 释放互斥锁
0. client#1 已获取到互斥锁
0. client#1 已再次获取到互斥锁
0. client#1 释放互斥锁
0. client#2 已获取到互斥锁
0. client#2 已再次获取到互斥锁
0. client#2 释放互斥锁
0. client#0 已获取到互斥锁
0. client#0 已再次获取到互斥锁
0. client#0 释放互斥锁
0. client#4 已获取到互斥锁
0. client#4 已再次获取到互斥锁
0. client#4 释放互斥锁
1. client#1 已获取到互斥锁
1. client#1 已再次获取到互斥锁
1. client#1 释放互斥锁
2. client#1 已获取到互斥锁
2. client#1 已再次获取到互斥锁
2. client#1 释放互斥锁
1. client#4 已获取到互斥锁
1. client#4 已再次获取到互斥锁
1. client#4 释放互斥锁
1. client#3 已获取到互斥锁
1. client#3 已再次获取到互斥锁
1. client#3 释放互斥锁
1. client#2 已获取到互斥锁
1. client#2 已再次获取到互斥锁
1. client#2 释放互斥锁
2. client#4 已获取到互斥锁
2. client#4 已再次获取到互斥锁
2. client#4 释放互斥锁
....
....
client#2 客户端关闭!
9. client#0 已获取到互斥锁
9. client#0 已再次获取到互斥锁
9. client#0 释放互斥锁
9. client#3 已获取到互斥锁
9. client#3 已再次获取到互斥锁
9. client#3 释放互斥锁
client#0 客户端关闭!
8. client#4 已获取到互斥锁
8. client#4 已再次获取到互斥锁
8. client#4 释放互斥锁
9. client#4 已获取到互斥锁
9. client#4 已再次获取到互斥锁
9. client#4 释放互斥锁
client#3 客户端关闭!
client#4 客户端关闭!
结束!

同时在程序运行期间查看Zookeeper节点树,可以发现每一次请求的锁实际上对应一个临时顺序节点

[zk: localhost:2181(CONNECTED) 42] ls /testZK/sharedreentrantlock
[leases, _c_208d461b-716d-43ea-ac94-1d2be1206db3-lock-0000001659, locks, _c_64b19dba-3efa-46a6-9344-19a52e9e424f-lock-0000001658, _c_cee02916-d7d5-4186-8867-f921210b8815-lock-0000001657]
2. 不可重入锁

Shared Lock 与 Shared Reentrant Lock 相似,但是不可重入,这个不可重入锁由类 InterProcessSemaphoreMutex 来实现,使用方法和上面的类似。

将上面程序中的 InterProcessMutex 换成不可重入锁 InterProcessSemaphoreMutex,如果再运行上面的代码,结果就会发现线程被阻塞在第二个 acquire 上,直到超时,也就是此锁不是可重入的。控制台输出日志如下:

0. client#2 已获取到互斥锁
0. client#1 不能得到互斥锁
0. client#4 不能得到互斥锁
0. client#0 不能得到互斥锁
0. client#3 不能得到互斥锁
client#1 客户端关闭!
client#4 客户端关闭!
client#3 客户端关闭!
client#0 客户端关闭!
0. client#2 释放互斥锁
0. client#2 不能再次得到互斥锁
client#2 客户端关闭!
结束!

把第二个获取锁的代码注释,程序才能正常执行

0. client#1 已获取到互斥锁
0. client#1 释放互斥锁
0. client#2 已获取到互斥锁
0. client#2 释放互斥锁
0. client#0 已获取到互斥锁
0. client#0 释放互斥锁
0. client#4 已获取到互斥锁
0. client#4 释放互斥锁
0. client#3 已获取到互斥锁
0. client#3 释放互斥锁
1. client#1 已获取到互斥锁
1. client#1 释放互斥锁
1. client#2 已获取到互斥锁
1. client#2 释放互斥锁
....
....
9. client#4 已获取到互斥锁
9. client#4 释放互斥锁
9. client#0 已获取到互斥锁
client#2 客户端关闭!
9. client#0 释放互斥锁
9. client#1 已获取到互斥锁
client#0 客户端关闭!
client#4 客户端关闭!
9. client#1 释放互斥锁
9. client#3 已获取到互斥锁
client#1 客户端关闭!
9. client#3 释放互斥锁
client#3 客户端关闭!
结束!
3. 可重入读写锁

Shared Reentrant Read Write Lock,可重入读写锁,一个读写锁管理一对相关锁,一个负责读操作,另外一个负责写操作;读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞);此锁是可重入的;一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁,这也意味着写锁可以降级成读锁, 比如 请求写锁 —>读锁 ---->释放写锁;从读锁升级成写锁是不行的。

可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex,使用时首先创建一个 InterProcessReadWriteLock 实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是 InterProcessMutex。

可以理解为我们上面分析的共享锁

 public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < clientNums; i++) {
            final String clientName = "client#" + i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    CuratorFramework client = ZKUtils.getClient();
                    client.start();
                    final InterProcessReadWriteLock lock = new InterProcessReadWriteLock(client, lockPath);
                    final InterProcessMutex readLock = lock.readLock();
                    final InterProcessMutex writeLock = lock.writeLock();

                    try {
                        // 注意只能先得到写锁再得到读锁,不能反过来!!!
                        if (!writeLock.acquire(10, TimeUnit.SECONDS)) {
                            throw new IllegalStateException(clientName + " 不能得到写锁");
                        }
                        System.out.println(clientName + " 已得到写锁");
                        if (!readLock.acquire(10, TimeUnit.SECONDS)) {
                            throw new IllegalStateException(clientName + " 不能得到读锁");
                        }
                        System.out.println(clientName + " 已得到读锁");
                        try {
                            resource.use(); // 使用资源
                        } finally {
                            System.out.println(clientName + " 释放读写锁");
                            readLock.release();
                            writeLock.release();
                        }
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    } finally {
                        CloseableUtils.closeQuietly(client);
                        countDownLatch.countDown();
                    }
                }
            }).start();
        }
        countDownLatch.await();
        System.out.println("结束!");
    }
}

控制台打印日志

client#1 已得到写锁
client#1 已得到读锁
client#1 释放读写锁
client#2 已得到写锁
client#2 已得到读锁
client#2 释放读写锁
client#0 已得到写锁
client#0 已得到读锁
client#0 释放读写锁
client#4 已得到写锁
client#4 已得到读锁
client#4 释放读写锁
client#3 已得到写锁
client#3 已得到读锁
client#3 释放读写锁
结束!
4. 信号量

Shared Semaphore,一个计数的信号量类似JDK的 Semaphore,JDK中 Semaphore 维护的一组许可(permits),而Cubator中称之为租约(Lease)。

有两种方式可以决定 semaphore 的最大租约数,在构造方法初始化的时候完成。第一种方式是由用户给定maxLeases 决定,第二种方式使用 SharedCountReader 类。

  public InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases)
  public InterProcessSemaphoreV2(CuratorFramework client, String path, SharedCountReader count)

信号量主要实现类有

InterProcessSemaphoreV2 - 信号量实现类
Lease - 租约(单个信号)
SharedCountReader - 计数器,用于计算最大租约数量

你可以请求多个租约,通过调用acquire方法完成。如果 Semaphore 当前的租约不够,则请求线程会被阻塞,同时还提供了超时的重载方法。

public Lease acquire() throws Exception
public Collection<Lease> acquire(int qty) throws Exception
public Lease acquire(long time, TimeUnit unit) throws Exception
public Collection<Lease> acquire(int qty, long time, TimeUnit unit) throws Exception

调用 acquire 会返回一个租约对象,客户端必须在 finally 中 close 这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉,那么这些客户端持有的租约会自动close,这样其它客户端可以继续使用这些租约。租约还可以通过下面的几个方法返还,可以返回一个,也可以返回多个。

public void returnLease(Lease lease)
public void returnAll(Collection<Lease> leases) 

一个Demo程序如下

public class SharedSemaphoreTest {
    private static final int MAX_LEASE = 10;
    private static final String PATH = "/testZK/semaphore";
    private static final FakeLimitedResource resource = new FakeLimitedResource();

    public static void main(String[] args) throws Exception {
        CuratorFramework client = ZKUtils.getClient();
        client.start();
        InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, PATH, MAX_LEASE);
        Collection<Lease> leases = semaphore.acquire(5);
        System.out.println("获取租约数量:" + leases.size());
        Lease lease = semaphore.acquire();
        System.out.println("获取单个租约");
        resource.use(); // 使用资源
        // 再次申请获取5个leases,此时leases数量只剩4个,不够,将超时
        Collection<Lease> leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);
        System.out.println("获取租约,如果超时将为null:" + leases2);
        System.out.println("释放租约");
        semaphore.returnLease(lease);
        // 再次申请获取5个,这次刚好够
        leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);
        System.out.println("获取租约,如果超时将为null:" + leases2);
        System.out.println("释放集合中的所有租约");
        semaphore.returnAll(leases);
        semaphore.returnAll(leases2);
        client.close();
        System.out.println("结束!");
    }
}

控制台打印日志

获取租约数量:5
获取单个租约
获取租约,如果超时将为null:null
释放租约
获取租约,如果超时将为null:[org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@3108bc, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@370736d9, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@5f9d02cb, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@63753b6d, org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2$3@6b09bb57]
释放集合中的所有租约
结束!

上面所讲的4种锁都是公平锁(fair)。从ZooKeeper的角度看,每个客户端都按照请求的顺序获得锁,相当公平。

5. 多锁

Multi Shared Lock 是一个锁的容器。当调用 acquire,所有的锁都会被 acquire,如果请求失败,所有的锁都会被 release。同样调用 release 时所有的锁都被 release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。

主要涉及两个类:

InterProcessMultiLock - 对所对象实现类
InterProcessLock - 分布式锁接口类

它的构造函数需要包含锁的集合,或者一组 ZooKeeper 的 path,用法和 Shared Lock 相

public InterProcessMultiLock(CuratorFramework client, List<String> paths)
public InterProcessMultiLock(List<InterProcessLock> locks)

一个Demo程序如下

public class MultiSharedLockTest {
    private static final String lockPath1 = "/testZK/MSLock1";
    private static final String lockPath2 = "/testZK/MSLock2";
    private static final FakeLimitedResource resource = new FakeLimitedResource();

    public static void main(String[] args) throws Exception {
        CuratorFramework client = ZKUtils.getClient();
        client.start();

        InterProcessLock lock1 = new InterProcessMutex(client, lockPath1); // 可重入锁
        InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2); // 不可重入锁
        // 组锁,多锁
        InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
        if (!lock.acquire(10, TimeUnit.SECONDS)) {
            throw new IllegalStateException("不能获取多锁");
        }
        System.out.println("已获取多锁");
        System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
        System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
        try {
            resource.use(); // 资源操作
        } finally {
            System.out.println("释放多个锁");
            lock.release(); // 释放多锁
        }
        System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
        System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
        client.close();
        System.out.println("结束!");
    }
}

读完是否有疑问?不妨留个言一起探讨,探讨!! 整理自互联网,未知出处,文章略有改动。

【zookeeper】zookeeper 简单介绍 Zookeeper 是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。Zookeeper 提供了一个类似于 Linux 文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。 阅读详情

相关推荐

【学长笔记】关于车牌识别的CCPD数据集按比例划分

本文介绍了一个用于CCPD车牌数据集划分的Python脚本,主要功能包括:1) 将数据集按比例划分为训练集、验证集和测试集;2) 支持图像与标签文件校验;3) 可自定义划分比例和样本数量限制;4) 自动生成标准目录结构。脚本使用随机种子确保可重复性,并通过tqdm显示进度条。处理后数据可直接用于YOLO等目标检测模型的训练与评估,建议在预处理完成后立即执行划分以避免数据泄露。文章还提供了相关技术博客的参考链接。

apple_53308981的博客 1478

SETNX分布锁详解

SETNX作为分布式锁详解

qq_47288175的博客 2万+

远距离长航时低成本泡沫垂直起降固定翼无人机技术详解

该技术通过采用泡沫材料制造无人机机体,并结合高效的垂直起降与固定翼飞行机制,实现了低成本、长航时、远距离的飞行能力。然而,需要注意的是,泡沫材料在强度和耐候性方面可能较传统金属材料有所不足,因此在设计和制造过程中需要采取额外的加固和防护措施。用于森林防火、水域监测、空气质量检测等场景,通过搭载相应的传感器和摄像设备,对大范围区域进行高效、精准的监测。- 增强智能化水平:集成更多先进的传感器、算法和人工智能技术,提升无人机的自主决策能力和作业效率。

专注多旋翼、复合翼无人机技术研究,从事低空经济行业。 1201

利用Zookeeper实现 - 分布式锁

许多场景中,数据一致性是一个比较重要的话题,在单机环境中,我们可以通过Java提供的并发API来解决;而在分布式环境(会遇到网络故障、消息重复、消息丢失等各种问题)下要复杂得多,常见的解...

JAVA葵花宝典 402

WorkBench3.2 + VxWorks6.8安装教程

按照FTP连接主机和目标机,将整个VxWorks6.8的安装分为两个部分,一是在主机(win10系统,可以使用虚拟机或者实体机,推荐使用虚拟机)上安装workbench3.2,进行bootrom与VxWroks映像文件的生成,二是在虚拟机上安装VxWorks系统 19.10.10 首先很高兴文章能帮到大家,但由于本人已不做该方向良久,私信和评论的很多问题现在已经无法解答,如果大家有什么问题欢迎...

Alive的博客 3万+

AI Agent技能评测框架SkillsBench:从黑盒评估到白盒技能拆解

在人工智能领域,模型评估是衡量技术进展的核心环节。传统评测基准如GLUE、MMLU主要关注模型的通用认知能力,但实际工程应用中,AI Agent需要的是在复杂场景中协调多种能力的“职业技能”。SkillsBench创新性地将Agent能力进行原子化拆解,定义了理解规划、工具调用、推理决策、交互安全等技能维度,并通过领域专家构建的评测集确保生态效度。这一框架为Agent开发提供了精准的“度量衡”,使性能优化从经验驱动转向数据驱动,同时为不同模型和框架提供了公平比较的基础,推动了AI Agent生态从“野蛮生长

weixin_30530339的博客 389

Redis分布式锁原理解析

Redis 分布式锁命令 setnx当且仅当 key 不存在。若给定的 key 已经存在,则 setnx不做任何动作。setnx 是『set if not exists』(如果不存在,则 set)的简写,setnx 具有原子性。 getset先 get 旧值,后set 新值,并返回 key 的旧值(old value),具有原子性。当 key 存在但不是字符串类型时,返回一个错误;当key ...

Andy86869的博客 1万+

(十五)CX20106A 中超声波测量的应用

(十五)CX20106A 中超声波测量的应用

m0_52555663的博客 4511

深入解析 Redis 分布式锁原理

深入解析 Redis 分布式锁原理

这个人很懒,什么都没有留下。 2871

利用ADS建立MIPI D-PHY链路仿真流程

由于MIPI D-PHY的速率并不算高,因此,PCB设计难度通常不大,主要做好差分线的等长、阻抗的连续性、端口处的阻抗优化以及控制好走线的长度,拿到满意的结果还是较为轻松的,最后,建立流程化的仿真工程文件,可以保存为模板,方便日后的设计复用,非常方便。

一只豌豆象的博客 3571

12、无人机系统的建模与仿真

本文深入探讨了无人机系统的建模与仿真技术,涵盖其发展需求、历史演变、动力学建模方法、常用参考框架与状态变量表示,并分析了在控制系统设计、操作员培训和子系统测试中的关键应用。文章还介绍了不同类型无人机(固定翼、旋翼、混合翼)在建模与仿真中的差异,提出了多学科融合、高精度实时仿真及VR/AR技术等未来发展趋势,并给出了实际建模与仿真的标准操作流程,为无人机系统的设计与优化提供了全面的技术支持与展望。

websocket5live的博客 196

redis分布式锁实现原理_面试必需要明白的 Redis 分布式锁实现原理!

点击蓝色“架构文摘”关注我哟加个“星标”,每天上午 09:25,干货推送!作者 | Vt来源 |juejin.im/post/5e61a454e51d4526f071e1df谈起redis锁,下面三个,算是出现最多的高频词汇:setnxredLockredissonsetnx其实目前通常所说的setnx命令,并非单指redis的setnx key value这条命令。一般代指redis...

weixin_39676979的博客 1387

脉冲变压器电感选型参数,结构原理,工艺与注意问题总结

脉冲变压器电感简称脉冲变压器,是一种特殊类型的变压器,主要用于处理脉冲信号。它的主要特性是能够在短时间内承受高电压和大电流。脉冲变压器主要用于开关电源、脉冲功率放大、雷达脉冲调制等领域。本文对脉冲变压器的工作原理,结构特点,工艺流程,选型参数及设计注意事项进行总结。

fydar的博客 1344

zookeeper 分布式锁_女朋友也能看懂的Zookeeper分布式锁原理

前言关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景。而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeeper,今天我们主要介绍的是基于zookeeper实现的分布式锁。这篇文章主要借用Curator框架对zk分布式锁的实现思路,大家理解了以后完全可以自己手动实现一遍,但是在工作中还是建议使用成熟的开源框架,很...

weixin_39679678的博客 75

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

架构师必知:分布式锁

在多节点的分布式系统中,控制多个客户端对共享资源的访问,确保同一时刻只有1个客户端能操作资源,避免数据错乱。• 比如库存只剩1件,2个线程同时查到“库存≥1”,都去扣减,最后库存变成-1,超卖了!• 核心:用“租约(Lease)”和“原子事务”实现,锁绑定租约,租约到期自动释放;:尽量缩小锁的范围(比如只锁库存扣减,不锁整个订单流程),能不用锁就用乐观锁;:分布式锁没有“银弹”,重点是平衡一致性、性能和实现复杂度,贴合自己的业务。(可选):同一客户端能多次拿同一把锁,或按请求顺序给锁(避免“饿死”)。

方才coding 1200

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

到底是谁说Java写分布式锁只能用Redis?你真不想多试试这些姿势吗?

你是不是一提到分布式锁,就只会说 Redis?哈!别不好意思承认,多少年做 Java 后端,我见过太多人张口就是“哎呀,分布式锁,那不就是 Redisson 吗?可你知道吗?光盯着 Redis,很多场景其实未必是最佳解!而且,做技术人,怎么能甘心只会一招呢?想想看——你愿意永远只靠同一把钥匙开所有的锁?各位江湖同道,大家好,我是你们那个在键盘敲字比在女朋友面前都能聊得多的 Java 后端老炮。今天这篇文章,我决定跟你聊聊一个老生常谈、却经常被说得“套路化”的话题——分布式锁。

欢迎来到菜鸟不学编程的博客 1173

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

千万不要给女朋友解释 什么是 “羊群效应”

今天和大家分享一道面试题,这个问题是前几天一个粉丝私下问的,题目其实比较简单,但这兄弟没有回答上来。 面试官:用过 zookeeper 吗? 同学:用过 面试官:那用 zookeeper 实现分布式锁如何避免 羊群效应? 同学:······· ...

程序员内点事 653

女友通话中?说我级别太低,没资格获取到锁!

在多节点的分布式系统中,控制多个客户端对共享资源的访问,确保同一时刻只有1个客户端能操作资源,避免数据错乱。• 比如库存只剩1件,2个线程同时查到“库存≥1”,都去扣减,最后库存变成-1,超卖了!• 核心:用“租约(Lease)”和“原子事务”实现,锁绑定租约,租约到期自动释放;:尽量缩小锁的范围(比如只锁库存扣减,不锁整个订单流程),能不用锁就用乐观锁;:分布式锁没有“银弹”,重点是平衡一致性、性能和实现复杂度,贴合自己的业务。(可选):同一客户端能多次拿同一把锁,或按请求顺序给锁(避免“饿死”)。

方才coding 1086

太悲伤了,女朋友面试惨遭面试官吊打,半夜偷偷摸摸躲在被子里抹眼泪刷题

集群部署时的分布式 session 如何实现?服务注册和发现是什么意思?Spring Cloud 如何实现?一般实现分布式锁都有哪些方式?使用 redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?dubbo 的 spi 思想是什么?如何设计可以动态扩容缩容的分库分表方案?四、分布式=====ActiveMQ 中的消息重发时间间隔和重发次数吗?Kafka 创建 Topic 时如何将分区放置到不同的 Broker 中。

2301_82242670的博客 653

zookeeper基本讲解

一、ZooKeeper概述 ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,是Google的Chubby一个开源实现,是Hadoop和Hbase的重要组件,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等。 zookeeper是基于...

qq_43193797的博客 376

阿里P7架构师的面试自述,短短一个春招面了字节,美团,腾讯阿里等大厂,这或许就是大佬的自我修养

微盟 一面: 项目 怎么优化数据库 索引的结构是什么 aop怎么用的 aop的底层怎么实现 反转字符串 社招第一场面试,还是比较紧张的,项目那块说的不太好。算法题感觉就是走个过场。。 多益 一面: 大部分是围绕项目来问的,感觉是技术面里面最水的。。而且一面通过之后还需要网申和IQ测评,特别麻烦。 字节 抖音电商 一面: 后序和中序,确定一棵树 口述算法:n的平方根 2T的文件 加载2G 出现次数top10 线程和进程的区别 进程怎么实现一个阻塞队列 进程间通信方式 反射是什么 类加载机制 ThreadLoc

欢迎您访问我的公众号【前程有光】 395

Java面试中的技术段子与工程实践解析

在Java开发领域,技术面试常常涉及核心概念如HashMap扩容机制、JVM内存模型和线程安全等。HashMap的扩容从JDK1.7的头插法改进为1.8的尾插法,解决了多线程环境下的死循环问题,而ConcurrentHashMap的分段锁机制则进一步提升了并发性能。JVM内存区域划分包括堆区、方法区和虚拟机栈,每部分在垃圾回收和程序运行中扮演关键角色。线程安全方案如synchronized、ReentrantLock和CAS各有适用场景,实际工程中需根据业务需求选择。Redis持久化策略AOF和RDB的混合

weixin_30940783的博客 294

每次面试的时候我都拿出来看一下,工资基本都涨5000以上

好家伙我要开始不害臊了 推一下自己的公众号:程序员内点事 这个号的绝大部分文章是自己在学习技术的过程中,实践总结的案例分享,像是redis、zookeeper、springboot 、flowable、分库分表Sharding-JDBC、RabbitMQ、seata、MySQL等等主流的一些框架,学技术做好的方式就是要理论加实战,这样理解的会更透彻 以下是我做这个号以来写的一些文章,我分了一下类,希望对你会有些帮助 Springboot 开发技巧 3种 Springboot 全局时间格式化方式,别再写重复

程序员内点事 655
上一篇: 灵魂发问!线程池到底创建多少线程比较合理?
下一篇: 面试Java后端开发总结心得,很走心的文章(附面试题)
yi博说
博客等级 码龄11年 20粉丝 30原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值