Curator实现信号量

背景

最近项目中对接某银行银企直连接口,对并发数量有非常严格的限制,比如说,在同一时刻并发数不能超过5个,那么在调用接口前就需要对请求数量 进行限制,既保证能在并发限制内发送请求(保证效率),还要保证处于等待的请求能顺序执行(保证正确性)。

分析

Curator实现信号量_第1张图片
业务整体就相当于我们在超市排队付款,总共只有三个收款口(A,B,C),也就是说同时只允许三个并发,而等待付款的人就在后面排队,按照先来后到的顺序,分别编号01,02,03,04.。。。当哪个口付完钱处于空闲状态时,后面排队的人就按顺序补上;此时C收款口付款完成了,那么等待的01号就可以去C口付钱。

那么如何实现呢?最一开始考虑的是通过线程池来实现,但是系统是集群环境,线程池的话并没有什么效果,当时想的是要是有一种分布式的线程池就好了= =

在寻找解决办法的过程中,想到了java的信号量是符合业务流程的,但如何做到分布式的呢?由此想到了java中的锁,对应了分布式锁,那么信号量也类似,应该也有分布式的,正巧项目中使用的分布式锁是Curator框架实现的,就研究了下Curator框架,发现Curator果然实现了分布式的信号量。

Curator信号量

java中叫信号量,Curator中叫租约,我们指定一个路径(比如/test),再指定一个最大租约数量(比如是3),每个请求过来需要消费一个租约,当请求完成后将占领的租约还回去,那么当请求数量少于等于3个的时候,可以直接通过,当正在执行的请求数量超过三个的时候,则按照先后顺序进行等待,直到正在执行的请求执行完成将占有的租约放回后,则按照顺序下一个等待的请求可以执行。

//指定/test路径,初始化最大租约数量3个
InterProcessSemaphoreV2  interProcessSemaphoreV2=new InterProcessSemaphoreV2(client, "/test", 3);
//从租约里取出一个租约
Collection<Lease> leases=interProcessSemaphoreV2.acquire(1);
if(leases!=null){//不为空说明获取到锁,可以执行
	System.out.println("获取租约1个成功");
	// 释放锁,将占领的一个租约还回去,
	interProcessSemaphoreV2.returnAll(leases);
}else{//为空说明未获取到锁
	System.out.println("获取租约1个失败");
}
//从租约里取出两个租约
Collection<Lease> leases2=interProcessSemaphoreV2.acquire(2);
if(leases2!=null){
	System.out.println("获取租约2个成功");
	interProcessSemaphoreV2.returnAll(leases2);
}else{
	System.out.println("获取租约2个失败");
}
//从租约里取出五个租约,如果获取不到锁,则等待60秒,超过60秒仍未获取到锁,则获取锁失败
Collection<Lease> lease3=interProcessSemaphoreV2.acquire(5, 60, TimeUnit.SECONDS);
if(lease3!=null){
	System.out.println("获取租约5个成功");
}else{
	System.out.println("获取租约5个失败");
}
interProcessSemaphoreV2.returnAll(lease3);

运行结果:生成租约3个,第一次获取租约1个成功,剩余2个租约,然后释放租约,剩余3个租约;第二次获取2个租约成功,剩余1个租约,然后释放租约;最后获取5个租约,因为租约数不足,等待60秒,如果等待期间租约数一直不足,则获取锁失败。

Curator是如何实现信号量的

首先Curator是基于zookeeper实现的锁操作,想要了解Curator的信号量,必须先了解基于zookeeper的分布式锁的实现原理。

了解zookeeper的分布式锁实现的人都知道,zookeeper是通过临时顺序节点和程序中的Watcher监听来实现的:
1.首先创建一个作为锁目录(znode),通常用它来描述锁定的实体,称为:/lock_node
2.希望获得锁的客户端在锁目录下创建znode,作为锁/lock_node的子节点,并且节点类型为有序临时节点(EPHEMERAL_SEQUENTIAL);
例如:有一个客户在多线程下创建znode,分别为/lock_node/lock000000001和/lock_node/lock00000002
3.当前客户端调用getChildren(/lock_node)得到锁目录所有子节点,不设置watch,接着获取小于自己(步骤2创建)的兄弟节点
4.步骤3中获取小于自己的节点不存在 && 最小节点与步骤2中创建的相同,说明当前客户端顺序号最小,获得锁,执行业务操作,删除本节点,结束。
5.客户端监视(watch)相对自己次小的有序临时节点状态
6.如果监视的次小节点状态发生变化,则跳转到步骤3,继续后续操作,直到退出锁竞争。

而Curator的信号量稍微复杂一些,获取锁的时候并不是创建一个节点,而是两个,分别为/leases/lock,这两个目录是干什么用的呢?我猜测,/lock目录下存放的节点值最小的那一个是下一个要执行的节点,而/leases下存放的是正在执行的节点+处于等待的节点信息(处于等待的节点不包括最后一个)
假设锁路径为/test,租约数量为2,Watcher监听/leases

  1. 线程T1尝试获取锁
  2. T1创建 /lock/001(LockInternals.internalLockLoop())
    Curator实现信号量_第2张图片
  3. T1创建/leases/001(InterProcessSemaphoreV2.internalAcquire1Lease())
    Curator实现信号量_第3张图片
  4. /leases下只有一个节点,不大于2个,可以获取到锁(InterProcessSemaphoreV2.internalAcquire1Lease())
    Curator实现信号量_第4张图片
  5. T1删除/lock/001节点(InterProcessSemaphoreV2.internalAcquire1Lease())
    Curator实现信号量_第5张图片
  6. T1执行业务。。。此时未释放锁

此时/test目录下的信息为:
/test/lock目录下为空
/test/lease目录下为/lease/001

第二个线程T2

  1. 线程T2尝试获取锁
  2. T2创建 /lock/002
  3. T2创建/leases/002
  4. /leases下有两个节点,不大于2个,可以获取到锁
  5. T2删除/lock/002节点
  6. T2执行业务。。。此时未释放锁

此时/test目录下的信息为:
/test/lock目录下为空
/test/lease目录下为/lease/001;/leases/002

第三个线程T3

  1. 线程T3尝试获取锁
  2. T3创建 /lock/003
  3. T3创建/leases/003
  4. /leases下有三个节点,大于2个,当前线程休眠(InterProcessSemaphoreV2.internalAcquire1Lease())
    Curator实现信号量_第6张图片

此时/test目录下的信息为:
/test/lock目录下为/lock/003
/test/lease目录下为/lease/001;/leases/002;/leases/003

第四个线程T4

  1. 线程T4尝试获取锁
  2. T4创建 /lock/004
  3. T4线程直接进入休眠(LockInternals.internalLockLoop())
    Curator实现信号量_第7张图片

此时/test目录下的信息为:
/test/lock目录下为/lock/003;/lock/004
/test/lease目录下为/lease/001;/leases/002;/leases/003

当T1执行完业务代码,释放锁时(释放锁就是删除/leases/001),此时目录信息为:
/test/lock目录下为/lock/003;/lock/004
/test/lease目录下为/leases/002;/leases/003;/leases/004
触发watcher(LockInternals()),而watcher只做了一件事,就是唤醒所有休眠的线程(LockInternals.notifyFromWatcher())
Curator实现信号量_第8张图片
在这里插入图片描述
唤醒线程后,则又进入了判断节点数量的步骤(InterProcessSemaphoreV2.internalAcquire1Lease())
Curator实现信号量_第9张图片
发现T3可以获取到锁了,则删除/lock/003,开始执行T3的业务代码,T4未获取到锁,继续休眠
此时/test目录下的信息为:
/test/lock目录下为/lock/004
/test/lease目录下为/leases/002;/leases/003;/leases/004

T2执行完业务代码,释放锁后,/test目录下的信息为:
/test/lock目录下为/lock/004
/test/lease目录下为/leases/003;/leases/004

watcher唤醒所有等待的线程(T4),开始判断节点数量的步骤(InterProcessSemaphoreV2.internalAcquire1Lease()),校验通过,则删除/lock/004,此时/test目录下的信息为:
/test/lock目录下为空
/test/lease目录下为/leases/003;/leases/004

接下来T3和T4执行完,释放锁后,因没有可唤起的线程,则本次操作结束。

你可能感兴趣的:(Curator)