客户端可以通过ZooKeeper的API来更新一个节点的数据内容,有如下两个接口:
- Stat setData(final String path, byte data[], int version)
- void setData(final String path, byte data[], int version, StatCallback cb, Object ctx)
这里列出的两个API分别是同步和异步的更新接口,API方法的参数说明如下表所示。
参数名 说明 path 指定数据节点的节点路径,即API调用的目的是更新该节点的数据内容 data[] 一个字节数组,即需要使用该数据内容来覆盖节点现在的数据内容 version 指定节点的数据版本,即表明本次更新操作室针对该数据版本进行的 cb 注册一个异步回调函数 ctx 用于传递上下文信息的对象
version参数
version参数用于指定节点的数据版本,表明本次更新操作是针对指定的数据版本进行的。具体来说,假如一个客户端试图进行更新操作,他会携带上次获取到的version值进行更新。而如果在这段时间内,ZooKeeper服务器上该节点的数据恰好已经被其他客户端更新了,那么其数据版本一定也发生了变化,因此肯定与客户端携带的version无法匹配,于是便无法更新成功——因此可以有效地避免一些分布式更新的并发问题,ZooKeeper的客户端就可以利用该特性构建更复杂的应用场景,例如分布式锁服务等。
使用同步API更新节点数据内容
// ZooKeeper API 更新节点数据内容,使用同步(sync)接口
public class SetData_API_Sync_Usage implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk;
public static void main(String[] args) throws Exception {
String path = "/zk-book";
zk = new ZooKeeper("domain1.book.zookeeper:2181", 5000, new SetData_API_Sync_Usage());
connectedSemaphore.await();
zk.create(path, "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
zk.getData(path, true, null);
Stat stat = zk.setData(path, "456".getBytes(), -1);
System.out.println(stat.getCzxid() + "," + stat.getMzxid() + "," + stat.getVersion());
Stat stat2 = zk.setData(path, "456".getBytes(), stat.getVersion());
System.out.println(stat2.getCzxid() + "," + stat2.getMzxid() + "," + stat2.getVersion());
try {
zk.setData(path, "456".getBytes(), stat.getVersion());
} catch (KeeperException e) {
System.out.println("Error: " + e.code() + "," + e.getMessage());
}
Thread.sleep(Integer.MAX_VALUE);
}
@Override
public void process(WatchedEvent event) {
if(KeeperState.SyncConnected == event.getState()) {
if(EventType.None == event.getType() && null == event.getPath()) {
connectedSemaphore.countDown();
}
}
}
}
运行程序,输出结果如下:
在上面的示例程序中,我们前后进行了三次更新操作,分别使用了不同的version,接下来我们针对这三次更新操作分别作讲解。
在第一次更新操作中,使用的版本是“-1”,并且更新成功。版本“-1”代表什么:在ZooKeeper中,数据版本都是从0开始计数的,所以严格的讲,“-1”并不是一个合法的数据版本,它仅仅是一个标识符,如果客户端传入的版本参数是“-1”,就是告诉ZooKeeper服务器,客户端需要基于数据的最新版本进行更新操作。如果对ZooKeeper数据节点的更新操作没有原子性要求,那么就可以使用“-1”.
第一次更新操作成功执行后,ZooKeeper服务端会返回给客户端一个数据节点的节点状态信息对象:stat,从这个数据结构中我们可以获取服务器上该节点的最新数据版本。从程序的运行情况可以看出,第一次更新操作完成后,节点的数据版本变更为“1”.于是在第二次更新操作中,我们在接口中传入了这个版本号,也执行成功,同时我们看到了,此时的数据版本已经变更为“2”了。
在进行第三次操作的时候,程序依然使用了之前的数据版本“1”来进行更新操作,于是更新失败了。
从上面这个例子中,我们可以看出,基于Version参数,可以很好的控制ZooKeeper上节点数据的原子性操作。
使用异步API更新节点数据内容
// ZooKeeper API 更新节点数据内容,使用异步(async)接口
public class SetData_API_ASync_Usage implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk;
public static void main(String[] args) throws Exception {
String path = "/zk-book";
zk = new ZooKeeper("domain1.book.zookeeper:2181", 5000, new SetData_API_ASync_Usage());
connectedSemaphore.await();
zk.create(path, "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
zk.setData(path, "456".getBytes(), -1, new IStatCallback(), null);
Thread.sleep(Integer.MAX_VALUE);
}
@Override
public void process(WatchedEvent event) {
if(KeeperState.SyncConnected == event.getState()) {
if(EventType.None == event.getType() && null == event.getPath()) {
connectedSemaphore.countDown();
}
}
}
}
class IStatCallback implements AsyncCallback.StatCallback {public void processResult(int rc, String path, Object ctx, Stat stat) {
if (rc == 0) {
System.out.println("SUCCESS");
}
}
}
异步API的使用和前面的例子基本类似,这里不再赘述。
CAS(Compare and Swap)理论
在现代绝大多数的计算机处理器体系架构中,都实现了对CAS的指令支持。通俗地讲,CAS的意义就是:“对于值V,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B。”ZooKeeper的setData接口中的version参数正式由CAS原理衍化而来的。