Shared Counter
管理着一个共享整型数据。所有的客户端都监听者同一个路径下的这个整型值的变化。
1. 关键 API
org.apache.curator.framework.recipes.shared.SharedCount
接口: org.apache.curator.framework.recipes.shared.SharedCountReader
监听器: org.apache.curator.framework.recipes.shared.SharedCountListener
2. 机制说明
本质上就是一个共享值(值,版本号)。
通过zk节点的版本号,对节点数值进行跟踪维护。
3. 用法
3.1 创建
public SharedCount(CuratorFramework client,
String path,
int seedValue)
- seedValue
- 如果path对应的节点没有创建,则使用这个初始值作为新创建的path节点的数据
3.2 使用
- SharedCounts使用之前,一定要调用:
count.start();
- 用完了之后,需要调用:
count.close();
- 获取当前值:
int getCount()
- 对计数器进行监听回调:
void addListener(SharedCountListener listener)
- 直接设值
public void setCount(int newCount)
- 尝试更新
- 只有当此客户端最后读取的值与zk中的值相等时,才能更新
- 而且调用此方法,不成功时,还会刷新本地缓存值
- 之后调用
getCount()
就是新值
public boolean trySetCount(int newCount)
4. 错误处理
SharedCountListener
继承于ConnectionStateListener
。对于一个启用的 SharedCount
需要添加监听器。而且必须注意链接状态的变化。
链接中断SUSPENDED
时,那在链接恢复RECONNECTED
之前,都应当认定计数器不准确。 而当链接丢失LOST
,那计数器就要认定不可用了。
5. 源码分析
5.1 类定义
public class SharedCount implements Closeable, SharedCountReader, Listenable<SharedCountListener>{}
实现3个接口:
java.io.Closeable
org.apache.curator.framework.listen.Listenable
- 这个接口定义了增加监听的入口方法
org.apache.curator.framework.recipes.shared.SharedCountReader
- 定义了取值方法
5.2 成员变量
public class SharedCount implements Closeable, SharedCountReader, Listenable<SharedCountListener>
{
private final Map<SharedCountListener, SharedValueListener> listeners = Maps.newConcurrentMap();
private final SharedValue sharedValue;
}
- listeners
java.util.concurrent.ConcurrentMap
- 让计数器监听器和值监听器对应起来
- sharedValue
org.apache.curator.framework.recipes.shared.SharedValue
- 共享值
public class SharedValue implements Closeable, SharedValueReader
{
private static final int UNINITIALIZED_VERSION = -1;
private final Logger log = LoggerFactory.getLogger(getClass());
private final ListenerContainer<SharedValueListener> listeners = new ListenerContainer<SharedValueListener>();
private final CuratorFramework client;
private final String path;
private final byte[] seedValue;
private final AtomicReference<State> state = new AtomicReference<State>(State.LATENT);
private final AtomicReference<VersionedValue<byte[]>> currentValue;
private final CuratorWatcher watcher = new CuratorWatcher()
{
@Override
public void process(WatchedEvent event) throws Exception
{
if ( state.get() == State.STARTED && event.getType() != Watcher.Event.EventType.None )
{
// don't block event thread in possible retry
readValueAndNotifyListenersInBackground();
}
}
};
private final ConnectionStateListener connectionStateListener = new ConnectionStateListener()
{
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState)
{
notifyListenerOfStateChanged(newState);
}
};
private enum State
{
LATENT,
STARTED,
CLOSED
}
}
SharedValue类中数据就丰富了:
- UNINITIALIZED_VERSION
- 私有常量
- 对于共享值,是通过版本号进行跟踪的,次常量就用于标识值还未初始化(同步)
- log
- listeners
- 一组
org.apache.curator.framework.recipes.shared.SharedValueListener
- 一组
- client
- path
- seedValue
- 字节数组
- path的初始值
- state
- 值的状态
AtomicReference
- 原子化引用
- 内部枚举
- LATENT
- STARTED
- CLOSED
- currentValue
- 当前值
org.apache.curator.framework.recipes.shared.VersionedValue
- 值
- 版本
AtomicReference
- 原子化引用
- watcher
- 监听器
- 通过这个监听器,在共享值启动之后,再添加真正的值监听器
- 异步添加监听器
- 防止事件线程在重试时阻塞
- connectionStateListener
- 链接状态监听器
5.3 构造器
public SharedCount(CuratorFramework client, String path, int seedValue)
{
sharedValue = new SharedValue(client, path, toBytes(seedValue));
}
就是对SharedValue
进行初始化
public SharedValue(CuratorFramework client, String path, byte[] seedValue)
{
this.client = client;
this.path = PathUtils.validatePath(path);
this.seedValue = Arrays.copyOf(seedValue, seedValue.length);
currentValue = new AtomicReference<VersionedValue<byte[]>>(new VersionedValue<byte[]>(UNINITIALIZED_VERSION, Arrays.copyOf(seedValue, seedValue.length)));
}
SharedValue的初始化过程也比较简单,并没有创建节点。
- 要注意初始的
currentValue
的版本是UNINITIALIZED_VERSION
5.4 启动
计数器在使用之前必须调用start()
方法。
public void start() throws Exception
{
sharedValue.start();
}
可以看出,实际上就是调用org.apache.curator.framework.recipes.shared.SharedValue#start
方法:
public void start() throws Exception
{
Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Cannot be started more than once");
client.getConnectionStateListenable().addListener(connectionStateListener);
try
{
client.create().creatingParentContainersIfNeeded().forPath(path, seedValue);
}
catch ( KeeperException.NodeExistsException ignore )
{
// ignore
}
readValue();
}
- 首先给链接加上监听器,监听链接状态
- 创建了一个普通节点
path
- 调用
readValue()
读取值
private void readValue() throws Exception
{
Stat localStat = new Stat();
byte[] bytes = client.getData().storingStatIn(localStat).usingWatcher(watcher).forPath(path);
updateValue(localStat.getVersion(), bytes);
}
- 获取
path
对应的节点值,以及节点状态信息 - 调用
updateValue()
进行更新
private void updateValue(int version, byte[] bytes)
{
while (true)
{
VersionedValue<byte[]> current = currentValue.get();
if (current.getVersion() >= version)
{
// A newer version was concurrently set.
return;
}
if ( currentValue.compareAndSet(current, new VersionedValue<byte[]>(version, bytes)) )
{
// Successfully set.
return;
}
// Lost a race, retry.
}
}
无锁化,不断重试:
- 用当前值的版本与新的版本号进行比较
- 如果当前值的版本号更大,则说明本地值更新,所以不需要更新
- 如果当前值的版本比较旧,则原子(重试)更新新值
5.5 获取值
SharedCount:
public int getCount()
{
return fromBytes(sharedValue.getValue());
}
@Override
public VersionedValue<Integer> getVersionedValue()
{
VersionedValue<byte[]> localValue = sharedValue.getVersionedValue();
return new VersionedValue<Integer>(localValue.getVersion(), fromBytes(localValue.getValue()));
}
只是格式化了数据。实际动作还是由:
SharedValue:
public byte[] getValue()
{
VersionedValue<byte[]> localCopy = currentValue.get();
return Arrays.copyOf(localCopy.getValue(),localCopy.getValue().length);
}
取值并不是实时从zk获取,而是读取的本地数据。返回数据副本,避免原始数值引用暴露出去导致数据被污染。
5.6 设置值
5.6.1 直接覆盖更新
SharedCount:
public void setCount(int newCount) throws Exception
{
sharedValue.setValue(toBytes(newCount));
}
SharedValue:
public void setValue(byte[] newValue) throws Exception
{
Preconditions.checkState(state.get() == State.STARTED, "not started");
Stat result = client.setData().forPath(path, newValue);
updateValue(result.getVersion(), Arrays.copyOf(newValue, newValue.length));
}
- 判断了链接状态
- 直接set更新zk节点
- 调用
update
更新本地值 也既是:
- 先更新ZK远程数据,再更新本地数据
- 使用zk节点的版本进行跟踪
5.6.1 安全更新
仅当本地数据与远程数据一致(版本)时,才更新为新值。如果发现远程数据已经变动,则更新操作失败,并同步远程数据到本地。
SharedCount:
@Deprecated
public boolean trySetCount(int newCount) throws Exception
{
return sharedValue.trySetValue(toBytes(newCount));
}
public boolean trySetCount(VersionedValue<Integer> previous, int newCount) throws Exception
{
VersionedValue<byte[]> previousCopy = new VersionedValue<byte[]>(previous.getVersion(), toBytes(previous.getValue()));
return sharedValue.trySetValue(previousCopy, toBytes(newCount));
}
直接使用值进行更新的方法已经不建议使用了。 推荐使用org.apache.curator.framework.recipes.shared.VersionedValue
的更新方法
SharedValue:
public boolean trySetValue(VersionedValue<byte[]> previous, byte[] newValue) throws Exception
{
Preconditions.checkState(state.get() == State.STARTED, "not started");
VersionedValue<byte[]> current = currentValue.get();
if ( previous.getVersion() != current.getVersion() || !Arrays.equals(previous.getValue(), current.getValue()) )
{
return false;
}
try
{
Stat result = client.setData().withVersion(previous.getVersion()).forPath(path, newValue);
updateValue(result.getVersion(), Arrays.copyOf(newValue, newValue.length));
return true;
}
catch ( KeeperException.BadVersionException ignore )
{
// ignore
}
readValue();
return false;
}
- 如果参数中的之前的数据版本或值,与当前本地数据不一致,则更新失败
- 使用ZK的版本CAS操作更新节点数值
- 如果失败,则不更新本地数据
- 重新读取新值
- 返回失败
- 如果失败,则不更新本地数据
- 更新本地数据
- 返回成功
5.7 数值监听
@Override
public void addListener(SharedCountListener listener)
{
addListener(listener, MoreExecutors.sameThreadExecutor());
}
@Override
public void addListener(final SharedCountListener listener, Executor executor)
{
SharedValueListener valueListener = new SharedValueListener()
{
@Override
public void valueHasChanged(SharedValueReader sharedValue, byte[] newValue) throws Exception
{
listener.countHasChanged(SharedCount.this, fromBytes(newValue));
}
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState)
{
listener.stateChanged(client, newState);
}
};
sharedValue.getListenable().addListener(valueListener, executor);
listeners.put(listener, valueListener);
}
- 如果不指定监听器的执行线程池,则使用
com.google.common.util.concurrent.MoreExecutors.SameThreadExecutorService
同一线程的回调执行。 - 监听计数器数值的变化
- 监听链接状态的变化
- 将
org.apache.curator.framework.recipes.shared.SharedCountListener
重新包装成org.apache.curator.framework.recipes.shared.SharedValueListener
- 将监听器添加到
SharedValue
中定义的org.apache.curator.framework.listen.ListenerContainer
里
5.8 关闭
SharedCount:
@Override
public void close() throws IOException
{
sharedValue.close();
}
SharedValue:
@Override
public void close() throws IOException
{
client.getConnectionStateListenable().removeListener(connectionStateListener);
state.set(State.CLOSED);
listeners.clear();
}
当共享值关闭使用时,并不会删除ZK节点,甚至连本地数据都没有清理,只是移除了监听器,更新了状态。
这意味着,关闭之后,仍然可以通过getValue
获取值。