[Curator] Shared Counter 的使用与分析

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();
}
  1. 首先给链接加上监听器,监听链接状态
  2. 创建了一个普通节点path
  3. 调用readValue()读取值
private void readValue() throws Exception
{
    Stat localStat = new Stat();
    byte[] bytes = client.getData().storingStatIn(localStat).usingWatcher(watcher).forPath(path);
    updateValue(localStat.getVersion(), bytes);
}
  1. 获取path对应的节点值,以及节点状态信息
  2. 调用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.
    }
}

无锁化,不断重试:

  1. 用当前值的版本与新的版本号进行比较
    1. 如果当前值的版本号更大,则说明本地值更新,所以不需要更新
    2. 如果当前值的版本比较旧,则原子(重试)更新新值

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));
}
  1. 判断了链接状态
  2. 直接set更新zk节点
  3. 调用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;
}
  1. 如果参数中的之前的数据版本或值,与当前本地数据不一致,则更新失败
  2. 使用ZK的版本CAS操作更新节点数值
    • 如果失败,则不更新本地数据
      1. 重新读取新值
      2. 返回失败
  3. 更新本地数据
    • 返回成功

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同一线程的回调执行。
  • 监听计数器数值的变化
  • 监听链接状态的变化
  1. org.apache.curator.framework.recipes.shared.SharedCountListener重新包装成org.apache.curator.framework.recipes.shared.SharedValueListener
  2. 将监听器添加到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获取值。

转载于:https://my.oschina.net/roccn/blog/916616

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值