一、序言
之前分享过关于Zookeeper客户端Curator的基本应用和高级应用,如果对Curator没有了解的可以先翻到文章底部看一下。本文分享关于Curator在分布式锁和分布式队列上的特性和应用。
二、分布式锁
2.1 可重入锁
Curator实现的可重入锁跟jdk的ReentrantLock类似,即可重入,意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞,由类InterProcessMutex来实现。
//实例化锁
InterProcessMutex lock = new InterProcessMutex(zkClient, path);
try {
lock.acquire();
/**
* TODO 业务逻辑
*/
}finally {
lock.release();
}
2.2 不可重入锁
这个锁和上面的InterProcessMutex相比,就是少了Reentrant的功能,也就意味着它不能在同一个线程中重入。这个类是InterProcessSemaphoreMutex,使用方法和InterProcessMutex类似。
InterProcessSemaphoreMutex lock = new InterProcessSemaphoreMutex(zkClient, path);
try {
lock.acquire(3, TimeUnit.SECONDS);
/**
* TODO 业务逻辑
*/
}finally {
lock.release();
}
2.3 可重入读写锁
Curator实现的可重入锁类似jdk的ReentrantReadWriteLock。一个读写锁管理一对相关的锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞)。
此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁, 比如请求写锁 —>请求读锁—>释放读锁 ---->释放写锁。从读锁升级成写锁是不行的。
可重入读写锁主要由两个类实现:InterProcessReadWriteLock、InterProcessMutex。使用时首先创建一个InterProcessReadWriteLock实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是InterProcessMutex。
// 实例化锁
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(zkClient, path);
// 获取读锁
InterProcessMutex readLock = lock.readLock();
// 获取写锁
InterProcessMutex writeLock = lock.writeLock();
try {
// 只能先得到写锁再得到读锁,不能反过来
if(!writeLock.acquire(3, TimeUnit.SECONDS)){
throw new IllegalStateException("acquire writeLock err");
}
if(!readLock.acquire(3, TimeUnit.SECONDS)){
throw new IllegalStateException("acquire readLock err");
}
/**
* TODO 业务逻辑
*/
}finally {
readLock.release();
writeLock.release();
}
2.4 信号量
Curator的信号量类似jdk的Semaphore,不同的是jdk中Semaphore维护的一组许可,而Curator中称之为租约。有两种方式可以决定semaphore的最大租约数,第一种方式是用户给定path并且指定最大LeaseSize。第二种方式用户给定path并且使用SharedCountReader类。
调用acquire()会返回一个租约对象,客户端必须在finally中close这些租约对象,否则这些租约会丢失掉。如果客户端session由于某种原因失效了, 那么这些客户端持有的租约会自动close, 这样其它客户端可以继续使用这些租约。租约还可以通过下面的方式返还:
public void returnAll(Collection<Lease> leases)
public void returnLease(Lease lease)
同时Curator可以一次性请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。
public Lease acquire()
public Collection<Lease> acquire(int qty)
public Lease acquire(long time, TimeUnit unit)
public Collection<Lease> acquire(int qty, long time, TimeUnit unit)
常用例子:
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(zkClient, path, 10);
// 只请求一个租约
Lease acquire = semaphore.acquire();
// 请求5个租约,同时设置超时时间
Collection<Lease> acquires = semaphore.acquire(5, 3, TimeUnit.SECONDS);
/**
* TODO
*/
// 释放所有
semaphore.returnAll(acquires);
// 只释放一个
semaphore.returnLease(acquire);
2.5 多共享锁对象
Multi Shared Lock是一个锁的容器,管理被它所包含的所有锁的状态。当调用acquire(),所有的锁都会被acquire(),如果请求失败,所有的锁都会被release,同样调用release成功时所有的锁也会被release。
@Test
public void multiLockTest() throws Exception{
/**省略zk客户端实例化代码**/
InterProcessLock lock1 = new InterProcessMutex(zkClient, path);
InterProcessLock lock2 = new InterProcessSemaphoreMutex(zkClient, path);
InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
if (!lock.acquire(3, TimeUnit.SECONDS)) {
throw new IllegalStateException("acquire lock err");
}
System.out.println("get lock success");
System.out.println("lock1: " + lock1.isAcquiredInThisProcess());
System.out.println("lock2: " + lock2.isAcquiredInThisProcess());
try {
TimeUnit.SECONDS.sleep(3);
} finally {
System.out.println("release lock");
lock.release();
}
System.out.println("lock1: " + lock1.isAcquiredInThisProcess());
System.out.println("lock2: " + lock2.isAcquiredInThisProcess());
}
新建一个InterProcessMultiLock, 包含一个重入锁和一个非重入锁, 调用acquire()后可以看到线程同时拥有了这两个锁。 调用release()看到这两个锁都被释放了。
三、分布式队列
Curator利用zk的顺序节点, 可以保证放入到队列中的内容是按照顺序排队的。那么对于单一的消费者来说就是先入先出的,这正好是队列的特点。 如果严格要求顺序,必须保证同一时刻只能有一个消费者,可以使用leader选举只让leader作为唯一的消费者。
但是zk并不适合做为队列,出于zk本身设计的限制或者说我们总能找到更合适做为队列的组件。我觉得主要考虑的有以下内容:
1.zk有1MB的传输限制,因而实践中最好保证znode必须相对较小,而队列包含成千上万的消息,非常的大,并非很合适。
2 znode很大的时候很难清理,当很大量的包含成千上万的子节点的znode时, zk的性能变得不好
网上也有人说是因为zk的数据库完全放在内存中,大量的Queue意味着会占用很多的内存空间。我觉得这并不是主要原因,主要还是存储数据量的考虑。像Redis数据也是保存在内存里,但拿它来做队列也挺合适。不过Curator还是创建了各种Queue的实现,数据量不太大的情况下也可以考虑使用。
3.1 普通的分布式队列
DistributedQueue是最普通的一种队列,包含4个类:
QueueBuilder:创建队列使用QueueBuilder,它也是其它队列的创建类
QueueConsumer:队列中的消息消费者接口
QueueSerializer:队列消息序列化和反序列化接口,提供了对队列中的对象的序列化和反序列化
DistributedQueue:队列实现类
QueueConsumer是消费者,它可以接收队列的数据,处理队列中的数据的代码逻辑则放在consumeMessage()中。
@Test
public void distributedQueueTest() throws Exception{
/**省略zk客户端实例化代码**/
QueueBuilder<String> builder = QueueBuilder.builder(zkClient, createQueueConsumer("consume"), createQueueSerializer(), path);
DistributedQueue<String> queue = builder.buildQueue();
queue.start();
try {
for (int i = 0; i < 100; i++){
// 往队列里添加数据
queue.put("message-" + i);
}
TimeUnit.SECONDS.sleep(10);
}finally {
queue.close();
}
}
/**
* 队列消息序列化实现类
*/
private static QueueSerializer<String> createQueueSerializer() {
return new QueueSerializer<String>() {
@Override
public byte[] serialize(String item) {
return item.getBytes();
}
@Override
public String deserialize(byte[] bytes) {
return new String(bytes);
}
};
}
/**
* 定义队列消费者
*/
private static QueueConsumer<String> createQueueConsumer(final String name) {
return new QueueConsumer<String>() {
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
System.out.println("连接状态改变: " + newState.name());
}
@Override
public void consumeMessage(String message) throws Exception {
System.out.println("消费消息(" + name + "): " + message);
}
};
}
同时,队列在添加数时候,可以为队列的每一个元素设置id,通过id可以移除队列中的指定元素。
// 放入元素
queue.put(aMessage, messageId);
// 移除元素
int numberRemoved = queue.remove(messageId);
3.2 优先级队列
优先级队列对队列中的元素按照优先级进行排序,Priority越小,元素越靠前,越先被消费掉。用法跟普通的队列差不多,不同的是创建队列返回的类型为DistributedPriorityQueue。
优先级队列可以通过builder.buildPriorityQueue(minItemsBeforeRefresh)创建。当优先级队列得到元素增删消息时,它会暂停处理当前的元素队列,然后刷新队列。minItemsBeforeRefresh指定刷新前当前活动的队列的最小数量,设置程序可以容忍的不排序的最小值。
@Test
public void priorityQueueTest() throws Exception{
/**省略zk客户端实例化代码**/
QueueBuilder<String> builder = QueueBuilder.builder(zkClient, createQueueConsumer("consume"), createQueueSerializer(), path);
DistributedPriorityQueue<String> queue = builder.buildPriorityQueue(0);
queue.start();
try {
for (int i = 0; i < 100; i++){
int priority = (int) (Math.random() * 100);
queue.put("message:" + i + " priority:" + priority, priority);
}
TimeUnit.SECONDS.sleep(500);
}finally {
queue.close();
}
}
有时候你可能会有错觉,优先级设置并没有起效。那是因为优先级是对于队列积压的元素而言,如果消费速度过快有可能出现在后一个元素入队操作之前前一个元素已经被消费,这种情况下DistributedPriorityQueue会退化为DistributedQueue。
3.3 延迟队列
Curator的延迟队列跟jdk中的延迟队列类似,元素有个delay值, 消费者隔一段时间才能收到内容。可以通过以下方式创建:
QueueBuilder<MessageType> builder = QueueBuilder.builder(client, consumer, serializer, path);
DistributedDelayQueue<MessageType> queue = builder.buildDelayQueue();
放入元素时可以指定延迟时间delayUntilEpoch:
queue.put(aMessage, delayUntilEpoch);
需要注意的是,delayUntilEpoch不是指离现在的一个时间间隔,而是指未来的一个时间戳,如 System.currentTimeMillis() + 10s,如果delayUntilEpoch的时间已经过去,消息会立刻被消费者接收。
@Test
public void DelayQueueTest() throws Exception{
/**省略zk客户端实例化代码**/
QueueBuilder<String> builder = QueueBuilder.builder(zkClient, createQueueConsumer("consume"), createQueueSerializer(), path);
DistributedDelayQueue<String> queue = builder.buildDelayQueue();
queue.start();
try {
queue.put("message", System.currentTimeMillis() + 3 * 1000);
TimeUnit.SECONDS.sleep(5);
}finally {
queue.close();
}
}
相关链接: