Distributed Atomic Long
分布式原子长整型
一个可以进行原子递增操作的计数器。 内部使用乐观锁实现,当失败时,再尝试加互斥排他锁。不论是乐观锁还是排他锁,都会按照重试策略进行重试操作。
1. 关键 API
org.apache.curator.framework.recipes.atomic.DistributedAtomicLong
org.apache.curator.framework.recipes.atomic.AtomicValue
org.apache.curator.framework.recipes.atomic.PromotedToLock
2. 机制说明
内部通过DistributedAtomicValue
来实现分布式原子数据的操作。
- 使用乐观锁
- 先尝试无锁的版本更新
- 不行再尝试加锁,并再次重试
- 使用重试策略管理
- 需要检查返回值是否成功(类似mongodb)
3. 用法
3.1 创建
- 乐观锁模式
public DistributedAtomicLong(CuratorFramework client,
String counterPath,
RetryPolicy retryPolicy)
- 需要指定重试策略
- 排他锁模式
public DistributedAtomicLong(CuratorFramework client,
String counterPath,
RetryPolicy retryPolicy,
PromotedToLock promotedToLock)
- 需要指定重试策略
- 配置锁的相关信息
3.2 使用
- 对数值的操作:
get()
increment()
decrement()
add()
subtract()
- 检查结果
- 操作完之后,必须调用
succeeded()
进行检查- 只有当返回true时,才能说明操作成功
- 如果操作成功,还可以:
preValue()
获取操作之前的值postValue()
获取操作之后的值
- 操作完之后,必须调用
4. 错误处理
这些原子实例的每一次方法调用都会访问ZK服务。所以,都会使用重试机制,而且一旦发生错误,就会抛出异常。
5. 源码分析
5.1 类定义
public interface DistributedAtomicNumber<T>{}
public class DistributedAtomicLong implements DistributedAtomicNumber<Long>{}
- 实现了
org.apache.curator.framework.recipes.atomic.DistributedAtomicNumber
接口- 定义了原子数值类型的基本操作api
AtomicValue<T> add(T delta)
- 增加,并返回新值
AtomicValue<T> compareAndSet(T expectedValue, T newValue)
- CAS更新,并返回新值
AtomicValue<T> decrement()
- 递减,并返回新值
void forceSet(T newValue)
- 强制覆盖更新
AtomicValue<T> get()
- 获取当前值
AtomicValue<T> increment()
- 递增,并返回新值
boolean initialize(T value)
- 初始化
- 如果设值成功,则返回true
- 如果节点已经存在,则返回false
AtomicValue<T> subtract(T delta)
- 减去,并返回新值
AtomicValue<T> trySet(T newValue)
- 尝试更新新值
- 需要调用
AtomicValue.succeeded()
,对操作结果进行检查
- 定义了原子数值类型的基本操作api
5.2 成员变量
public class DistributedAtomicLong implements DistributedAtomicNumber<Long>
{
private final DistributedAtomicValue value;
}
很简单,只定义了一个org.apache.curator.framework.recipes.atomic.DistributedAtomicValue
public class DistributedAtomicValue
{
private final CuratorFramework client;
private final String path;
private final RetryPolicy retryPolicy;
private final PromotedToLock promotedToLock;
private final InterProcessMutex mutex;
}
在DistributedAtomicValue中,定义了:
- retryPolicy
- 重试策略
- promotedToLock
org.apache.curator.framework.recipes.atomic.PromotedToLock
- 锁升级的相关信息
- mutex
org.apache.curator.framework.recipes.locks.InterProcessMutex
- 互斥锁
发现
DistributedAtomicNumber
的大部分操作都是通过value
来完成的。
5.3 构造器
public DistributedAtomicLong(CuratorFramework client, String counterPath, RetryPolicy retryPolicy)
{
this(client, counterPath, retryPolicy, null);
}
public DistributedAtomicLong(CuratorFramework client, String counterPath, RetryPolicy retryPolicy, PromotedToLock promotedToLock)
{
value = new DistributedAtomicValue(client, counterPath, retryPolicy, promotedToLock);
}
都是为了初始化DistributedAtomicValue
public DistributedAtomicValue(CuratorFramework client, String path, RetryPolicy retryPolicy, PromotedToLock promotedToLock)
{
this.client = client;
this.path = PathUtils.validatePath(path);
this.retryPolicy = retryPolicy;
this.promotedToLock = promotedToLock;
mutex = (promotedToLock != null) ? new InterProcessMutex(client, promotedToLock.getPath()) : null;
}
需要注意默认情况下,mutex
使用的是InterProcessMutex
5.4 初始化
分布式原子数值,在使用之前需要调用initialize
进行初始化。否则,取值就会是null。
DistributedAtomicLong:
public boolean initialize(Long initialize) throws Exception
{
return value.initialize(valueToBytes(initialize));
}
DistributedAtomicValue:
public boolean initialize(byte[] value) throws Exception
{
try
{
client.create().creatingParentContainersIfNeeded().forPath(path, value);
}
catch ( KeeperException.NodeExistsException ignore )
{
// ignore
return false;
}
return true;
}
- 创建一个普通节点
- 使用initialize值作为节点数据
5.5 获取值
DistributedAtomicLong:
public AtomicValue<Long> get() throws Exception
{
return new AtomicLong(value.get());
}
对于DistributedAtomicValue返回的AtomicValue
对象,使用了一个内部类org.apache.curator.framework.recipes.atomic.DistributedAtomicLong.AtomicLong
进行了包装。
DistributedAtomicValue:
public AtomicValue<byte[]> get() throws Exception
{
MutableAtomicValue<byte[]> result = new MutableAtomicValue<byte[]>(null, null, false);
getCurrentValue(result, new Stat());
result.postValue = result.preValue;
result.succeeded = true;
return result;
}
private boolean getCurrentValue(MutableAtomicValue<byte[]> result, Stat stat) throws Exception
{
boolean createIt = false;
try
{
result.preValue = client.getData().storingStatIn(stat).forPath(path);
}
catch ( KeeperException.NoNodeException e )
{
result.preValue = null;
createIt = true;
}
return createIt;
}
- 返回结果是
org.apache.curator.framework.recipes.atomic.MutableAtomicValue
- 从名字中也能看出这个对象是不保障原子操作的
- 从
path
节点中获取数据- 如果节点不存在
- 还未初始化
- 则数值为null
- 如果节点不存在
5.6 强制覆盖更新
DistributedAtomicLong:
public void forceSet(Long newValue) throws Exception
{
value.forceSet(valueToBytes(newValue));
}
DistributedAtomicValue:
public void forceSet(byte[] newValue) throws Exception
{
try
{
client.setData().forPath(path, newValue);
}
catch ( KeeperException.NoNodeException dummy )
{
try
{
client.create().creatingParentContainersIfNeeded().forPath(path, newValue);
}
catch ( KeeperException.NodeExistsException dummy2 )
{
client.setData().forPath(path, newValue);
}
}
}
按不同的概率进行了三种赋值动作,也为了减少多个创建节点动作的冲突。也是一种乐观设计。
- 对zk节点进行常规写操作
- 如果节点不存在,再创建,并赋值
- 如果节点已存在(上一步有并发,其他人创建了节点)
- 再次对zk节点进行常规写操作
- 如果节点已存在(上一步有并发,其他人创建了节点)
- 如果节点不存在,再创建,并赋值
5.7 尝试更新新值
DistributedAtomicLong:
public AtomicValue<Long> trySet(Long newValue) throws Exception
{
return new AtomicLong(value.trySet(valueToBytes(newValue)));
}
DistributedAtomicValue:
public AtomicValue<byte[]> trySet(final byte[] newValue) throws Exception
{
MutableAtomicValue<byte[]> result = new MutableAtomicValue<byte[]>(null, null, false);
MakeValue makeValue = new MakeValue()
{
@Override
public byte[] makeFrom(byte[] previous)
{
return newValue;
}
};
tryOptimistic(result, makeValue);
if ( !result.succeeded() && (mutex != null) )
{
tryWithMutex(result, makeValue);
}
return result;
}
private void tryOptimistic(MutableAtomicValue<byte[]> result, MakeValue makeValue) throws Exception
{
long startMs = System.currentTimeMillis();
int retryCount = 0;
boolean done = false;
while ( !done )
{
result.stats.incrementOptimisticTries();
if ( tryOnce(result, makeValue) )
{
result.succeeded = true;
done = true;
}
else
{
if ( !retryPolicy.allowRetry(retryCount++, System.currentTimeMillis() - startMs, RetryLoop.getDefaultRetrySleeper()) )
{
done = true;
}
}
}
result.stats.setOptimisticTimeMs(System.currentTimeMillis() - startMs);
}
private boolean tryOnce(MutableAtomicValue<byte[]> result, MakeValue makeValue) throws Exception
{
Stat stat = new Stat();
boolean createIt = getCurrentValue(result, stat);
boolean success = false;
try
{
byte[] newValue = makeValue.makeFrom(result.preValue);
if ( createIt )
{
client.create().creatingParentContainersIfNeeded().forPath(path, newValue);
}
else
{
client.setData().withVersion(stat.getVersion()).forPath(path, newValue);
}
result.postValue = Arrays.copyOf(newValue, newValue.length);
success = true;
}
catch ( KeeperException.NodeExistsException e )
{
// do Retry
}
catch ( KeeperException.BadVersionException e )
{
// do Retry
}
catch ( KeeperException.NoNodeException e )
{
// do Retry
}
return success;
}
private void tryWithMutex(MutableAtomicValue<byte[]> result, MakeValue makeValue) throws Exception
{
long startMs = System.currentTimeMillis();
int retryCount = 0;
if ( mutex.acquire(promotedToLock.getMaxLockTime(), promotedToLock.getMaxLockTimeUnit()) )
{
try
{
boolean done = false;
while ( !done )
{
result.stats.incrementPromotedTries();
if ( tryOnce(result, makeValue) )
{
result.succeeded = true;
done = true;
}
else
{
if ( !promotedToLock.getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMs, RetryLoop.getDefaultRetrySleeper()) )
{
done = true;
}
}
}
}
finally
{
mutex.release();
}
}
result.stats.setPromotedTimeMs(System.currentTimeMillis() - startMs);
}
定义了一个org.apache.curator.framework.recipes.atomic.MakeValue
- 拿到旧值之后如何转换成新值的装换器
- 首先调用
tryOptimistic
,进行乐观尝试- 按照重试策略,不断重试调用
tryOnce
- 读取当前数值
- 如果节点还不存在,则直接以新值进行节点创建
- 如果节点已经存在,则使用当前数值版本进行更新
- 将新值赋值保存
- 返回成功
- 任何zk异常,都忽略了,因为可以重试
- 如果重试策略认定不再重试了,则退出循环
- 记录乐观尝试所消耗的时间
- 按照重试策略,不断重试调用
- 如果尝试不成功,并且设置了排他锁(默认是带锁的),则尝试加锁操作
tryWithMutex
- 按照
promotedToLock
中配置的锁等待时间,申请排他锁- 加锁成功后,再次尝试
tryOnce
- 再次使用重试策略
- 释放锁
- 加锁成功后,再次尝试
- 记录加锁操作所消耗的时间
- 按照
5.8 递增
DistributedAtomicLong:
public AtomicValue<Long> increment() throws Exception
{
return worker(1L);
}
private AtomicValue<Long> worker(final Long addAmount) throws Exception
{
Preconditions.checkNotNull(addAmount, "addAmount cannot be null");
MakeValue makeValue = new MakeValue()
{
@Override
public byte[] makeFrom(byte[] previous)
{
long previousValue = (previous != null) ? bytesToValue(previous) : 0;
long newValue = previousValue + addAmount;
return valueToBytes(newValue);
}
};
AtomicValue<byte[]> result = value.trySet(makeValue);
return new AtomicLong(result);
}
- 通过构建一个
org.apache.curator.framework.recipes.atomic.MakeValue
来定义如何产生新值- 参见上一节
- 调用
DistributedAtomicValue
的trySet
方法,进行尝试更新- 参见上一节
可见,递增方法也是需要AtomicValue.succeeded()
方法
5.9 减值
DistributedAtomicLong:
public AtomicValue<Long> subtract(Long delta) throws Exception
{
return worker(-1 * delta);
}
逻辑,参见上一节
可见,此方法也是需要AtomicValue.succeeded()
方法
6 小结
DistributedAtomicLong
的操作大部分都是使用DistributedAtomicValue
的trySet
方法。
所以,使用时务必需要检查返回的AtomicValue
对象的succeeded()
方法。
对比org.apache.curator.framework.recipes.shared.SharedCount
,DistributedAtomicLong
能够更细节,更安全(原子操作,重试策略)的控制数值步进。
除了长整型,还有org.apache.curator.framework.recipes.atomic.DistributedAtomicInteger
,逻辑和用法都和DistributedAtomicLong
类似