最近项目中对接某银行银企直连接口,对并发数量有非常严格的限制,比如说,在同一时刻并发数不能超过5个,那么在调用接口前就需要对请求数量 进行限制,既保证能在并发限制内发送请求(保证效率),还要保证处于等待的请求能顺序执行(保证正确性)。
业务整体就相当于我们在超市排队付款,总共只有三个收款口(A,B,C),也就是说同时只允许三个并发,而等待付款的人就在后面排队,按照先来后到的顺序,分别编号01,02,03,04.。。。当哪个口付完钱处于空闲状态时,后面排队的人就按顺序补上;此时C收款口付款完成了,那么等待的01号就可以去C口付钱。
那么如何实现呢?最一开始考虑的是通过线程池来实现,但是系统是集群环境,线程池的话并没有什么效果,当时想的是要是有一种分布式的线程池就好了= =
在寻找解决办法的过程中,想到了java的信号量是符合业务流程的,但如何做到分布式的呢?由此想到了java中的锁,对应了分布式锁,那么信号量也类似,应该也有分布式的,正巧项目中使用的分布式锁是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是基于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
:
/lock/001
(LockInternals.internalLockLoop())/leases/001
(InterProcessSemaphoreV2.internalAcquire1Lease())/leases
下只有一个节点,不大于2个,可以获取到锁(InterProcessSemaphoreV2.internalAcquire1Lease())/lock/001
节点(InterProcessSemaphoreV2.internalAcquire1Lease())此时/test
目录下的信息为:
/test/lock
目录下为空
/test/lease
目录下为/lease/001
第二个线程T2
/lock/002
/leases/002
/leases
下有两个节点,不大于2个,可以获取到锁/lock/002
节点此时/test
目录下的信息为:
/test/lock
目录下为空
/test/lease
目录下为/lease/001;/leases/002
第三个线程T3
/lock/003
/leases/003
/leases
下有三个节点,大于2个,当前线程休眠(InterProcessSemaphoreV2.internalAcquire1Lease())此时/test
目录下的信息为:
/test/lock
目录下为/lock/003
/test/lease
目录下为/lease/001;/leases/002;/leases/003
第四个线程T4
此时/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())
唤醒线程后,则又进入了判断节点数量的步骤(InterProcessSemaphoreV2.internalAcquire1Lease())
发现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执行完,释放锁后,因没有可唤起的线程,则本次操作结束。