版本 | 日期 | 备注 |
---|---|---|
1.0 | 2020.4.19 | 文章首发 |
1.1 | 2021.6.23 | 标题从深入浅出Zookeeper(五):BadVersionException到底是怎么一回事 改变为深入浅出Zookeeper源码(五):BadVersionException到底是怎么一回事 |
前言
最近在开发时偶尔会观测到zk报出BadVersionException
,后在搜索引起上得知了是乐观锁相关的问题,很快就解决了问题。不过学而不思则罔:无论是单体应用还是分布式系统,在运行过程中总要有一种机制来保证数据排他性。接下来,我们就来看看zk是如何实现这种机制的。
节点属性
在此分析源码之前,我们需要了解zk节点的三种版本属性:
- version: 当前数据节点数据内容的版本号
- cversion: 当前数据子节点的版本号
- aversion: 当前数据节点ACL变更版本号
这些属性都可以在
StatPersisted
这个类里找到。
当相关的属性进行变更时,版本号则会+1。刚创建的节点,版本号为0,表示这个节点被更新过0次。
源码分析
一般如果我们调用setData
,代码会这么写:
//Curator版本
//要求版本对比。当然,填-1服务端在接收时便不会去对比了
client.setData().withVersion(version).forPath(path, payload);
//不要求版本对比
client.setData().forPath(path, payload);
zookeeper的client代码非常简单:
/**
* The asynchronous version of setData.
*
* @see #setData(String, byte[], int)
*/
public void setData(final String path, byte data[], int version,
StatCallback cb, Object ctx)
{
final String clientPath = path;
PathUtils.validatePath(clientPath);
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.setData);
SetDataRequest request = new SetDataRequest();
request.setPath(serverPath);
request.setData(data);
request.setVersion(version);
SetDataResponse response = new SetDataResponse();
cnxn.queuePacket(h, new ReplyHeader(), request, response, cb,
clientPath, serverPath, ctx, null);
}
之后version这个属性会被序列化到请求中,发送给服务端。
看下服务端的代码。从异常的名字,我们可以很轻易的找到代码PrepRequestProcessor.pRequest2Txn
里的代码:
case OpCode.setData:
zks.sessionTracker.checkSession(request.sessionId, request.getOwner());
SetDataRequest setDataRequest = (SetDataRequest)record;
if(deserialize)
ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest);
path = setDataRequest.getPath();
validatePath(path, request.sessionId);
nodeRecord = getRecordForPath(path);
checkACL(zks, nodeRecord.acl, ZooDefs.Perms.WRITE, request.authInfo);
int newVersion = checkAndIncVersion(nodeRecord.stat.getVersion(), setDataRequest.getVersion(), path);
request.setTxn(new SetDataTxn(path, setDataRequest.getData(), newVersion));
nodeRecord = nodeRecord.duplicate(request.getHdr().getZxid());
nodeRecord.stat.setVersion(newVersion);
addChangeRecord(nodeRecord);
break;
我们看一下checkAndIncVersion
的逻辑:
private static int checkAndIncVersion(int currentVersion, int expectedVersion, String path)
throws KeeperException.BadVersionException {
if (expectedVersion != -1 && expectedVersion != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
return currentVersion + 1;
}
代码简单易懂:从zk里取出该节点的版本,如果请求需要对比(由客户端设置不为-1)则与节点目前的版本进行对比。
如果没有抛出异常,则这个版本号会被+1,并更新提交到队列里去,最后会更新到zk的内存数据库中去。
很显然,这是CAS技术的一种实现。那么为什么要基于CAS实现锁呢?在此之前,我们需要回顾乐观锁和悲观锁的适用场景:
- 悲观锁:适用于那些对于数据更新竞争十分激烈的场景。因为其具有强烈的独占性和排他特性。
- 乐观锁:适用于数据并发竞争不大、事务冲突较少的场景。其不依靠独占来实现锁,较常见的实现是我们刚提到的CAS。
我们都知道,zk一般用于配置管理、DNS服务、分布式协同和组成员管理
,这意味着较少的数据并发竞争,而事务其实也是由leader服务器串行处理。显然,这符合乐观锁的使用场景,故此zk没有采用“笨重”的悲观锁来实现分布式数据的原子性操作。
小结
在本文中,我们得知zk的数据排他性机制实现是乐观锁。这么设计的原因是zk典型使用场景数据并发竞争的情况较少(当然,你可以让它竞争很激烈,只是整体来看过程会变得较为耗时),且事务操作都是串行执行。