这一篇文章我们将学习使用Curator来实现计数器。顾名思义,计数器是用来计数的,利用ZooKeeper可以实现一个集群共享的计数器。只要使用相同的path就可以得到最新的计数器值,这是由ZooKeeper的一致性保证的。Curator有两个计数器,一个是用int来计数,一个用long来计数。
1.SharedCount
1.SharedCount计数器介绍
这个类使用int类型来计数。 主要涉及三个类。
- SharedCount - 管理一个共享的整数。所有看同样的路径客户端将有共享的整数(考虑ZK的正常一致性保证)的最高最新的值。
- SharedCountReader - 一个共享的整数接口,并允许监听改变它的值。
- SharedCountListener - 用于监听共享整数发生变化的监听器。
SharedCount类代表计数器,可以为它增加一个SharedCountListener,当计数器改变时此Listener可以监听到改变的事件,而SharedCountReader可以读取到最新的值,包括字面值和带版本信息的值VersionedValue。
注意:使用SharedCount之前需要调用start(),使用完成之后需要调用stop()
2.编写示例程序
public class SharedCounterExample implements SharedCountListener
{
private static final int QTY = 5;
private static final String PATH = "/examples/counter";
public static void main(String[] args) throws IOException, Exception
{
final Random rand = new Random();
SharedCounterExample example = new SharedCounterExample();
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
SharedCount baseCount = new SharedCount(client, PATH, 0);
baseCount.addListener(example);
baseCount.start();
List<SharedCount> examples = Lists.newArrayList();
ExecutorService service = Executors.newFixedThreadPool(QTY);
for (int i = 0; i < QTY; ++i)
{
final SharedCount count = new SharedCount(client, PATH, 0);
examples.add(count);
Callable<Void> task = new Callable<Void>()
{
@Override
public Void call() throws Exception
{
count.start();
Thread.sleep(rand.nextInt(10000));
count.setCount(rand.nextInt(10000));
System.out.println("计数器当前值:" + count.getVersionedValue().getValue());
System.out.println("计数器当前版本:" + count.getVersionedValue().getVersion());
System.out.println("trySetCount:" + count.trySetCount(count.getVersionedValue(), 123));
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
for (int i = 0; i < QTY; ++i)
{
examples.get(i).close();
}
baseCount.close();
client.close();
System.out.println("OK!");
}
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState)
{
System.out.println("连接状态: " + newState.toString());
}
@Override
public void countHasChanged(SharedCountReader sharedCount, int newCount) throws Exception
{
System.out.println("计数器值改变:" + newCount);
}
}
在这个例子中,我们使用baseCount来监听计数值(addListener方法)。任意的SharedCount,只要使用相同的PATH,都可以得到这个计数值。然后我们使用5个线程为计数值增加一个10以内的随机数。
这里我们使用trySetCount去设置计数器。第一个参数提供当前的VersionedValue,如果期间其它client更新了此计数值,你的更新可能不成功,但是这时你的client更新了最新的值,所以失败了你可以尝试再更新一次。而setCount是强制更新计数器的值。
注意:计数器必须start,使用完之后必须调用close关闭它。
在这里再重复一遍前面讲到的, 强烈推荐你监控ConnectionStateListener, 尽管我们的有些例子没有监控它。 在本例中SharedCountListener扩展了ConnectionStateListener。 这一条针对所有的Curator recipes都适用,后面的文章中就不专门提示了。
3.示例程序运行结果
运行结果控制台:
连接状态: CONNECTED
计数器当前值:1684
计数器当前版本:11
trySetCount:true
计数器值改变:123
计数器当前值:8425
计数器当前版本:13
trySetCount:true
计数器值改变:123
计数器当前值:9369
计数器当前版本:15
trySetCount:true
计数器值改变:123
计数器当前值:4075
计数器当前版本:17
trySetCount:true
计数器值改变:123
计数器当前值:9221
计数器当前版本:19
trySetCount:true
OK!
Zookeeper节点信息如下:
2.DistributedAtomicLong
再看一个Long类型的计数器。除了计数的范围比SharedCount大了之外,它首先尝试使用乐观锁的方式设置计数器,如果不成功(比如期间计数器已经被其它client更新了),它使用InterProcessMutex方式来更新计数值。还记得InterProcessMutex是什么吗?它是我们前面讲的分布式可重入锁。这和上面的计数器的实现有显著的不同。
1.DistributedAtomicLong计数器介绍
DistributedAtomicLong计数器和上面的计数器的实现有显著的不同,可以从它的内部实现DistributedAtomicValue.trySet中看出端倪。
public class DistributedAtomicLong implements DistributedAtomicNumber<Long>
{
private final DistributedAtomicValue value;
......
}
public class DistributedAtomicValue
{
......
AtomicValue<byte[]> trySet(MakeValue makeValue) throws Exception
{
MutableAtomicValue<byte[]> result = new MutableAtomicValue<byte[]>(null, null, false);
tryOptimistic(result, makeValue);
if ( !result.succeeded() && (mutex != null) )
{
tryWithMutex(result, makeValue);
}
return result;
}
......
}
此计数器有一系列的操作:
- get(): 获取当前值
- increment(): 加一
- decrement(): 减一
- add(): 增加特定的值
- subtract(): 减去特定的值
- trySet(): 尝试设置计数值
- forceSet(): 强制设置计数值
你必须检查返回结果的succeeded(),它代表此操作是否成功。如果操作成功,preValue()代表操作前的值,postValue()代表操作后的值。
2.编写示例程序
我们下面的例子中使用5个线程对计数器进行加一操作,如果成功,将操作前后的值打印出来。
public class DistributedAtomicLongExample
{
private static final int QTY = 5;
private static final String PATH = "/examples/counter";
public static void main(String[] args) throws IOException, Exception
{
final Random rand = new Random();
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
client.start();
List<DistributedAtomicLong> examples = Lists.newArrayList();
ExecutorService service = Executors.newFixedThreadPool(QTY);
for (int i = 0; i < QTY; ++i)
{
final DistributedAtomicLong count = new DistributedAtomicLong(client, PATH, new RetryNTimes(10, 10));
examples.add(count);
Callable<Void> task = new Callable<Void>()
{
@Override
public Void call() throws Exception
{
try
{
Thread.sleep(1000 + rand.nextInt(10000));
AtomicValue<Long> value = count.increment();
System.out.println("修改成功: " + value.succeeded());
if (value.succeeded())
{
System.out.println("修改之前的值:" + value.preValue() + " | 修改之后的值:" + value.postValue());
}
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
};
service.submit(task);
}
service.shutdown();
service.awaitTermination(10, TimeUnit.MINUTES);
client.close();
System.out.println("OK!");
}
}
注意:你必须检查返回结果的succeeded(),它代表此操作是否成功。如果操作成功,preValue()代表操作前的值,postValue()代表操作后的值。
3.示例程序运行结果
运行结果控制台:
修改成功: true
修改之前的值:0 | 修改之后的值:1
修改成功: true
修改之前的值:1 | 修改之后的值:2
修改成功: true
修改之前的值:2 | 修改之后的值:3
修改成功: true
修改之前的值:3 | 修改之后的值:4
修改成功: true
修改之前的值:4 | 修改之后的值:5
OK!
Zookeeper节点信息如下: