2
本文首发于泊浮目的简书:https://www.jianshu.com/u/204...
版本 日期 备注
1.0 2020.4.19 文章首发

前言

最近在开发时偶尔会观测到zk报出BadVersionException,后在搜索引起上得知了是乐观锁相关的问题,很快就解决了问题。不过学而不思则罔:无论是单体应用还是分布式系统,在运行过程中总要有一种机制来保证数据排他性。接下来,我们就来看看zk是如何实现这种机制的。

节点属性

在此分析源码之前,我们需要了解zk节点的三种版本属性:

  1. version: 当前数据节点数据内容的版本号
  2. cversion: 当前数据子节点的版本号
  3. 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典型使用场景数据并发竞争的情况较少(当然,你可以让它竞争很激烈,只是整体来看过程会变得较为耗时),且事务操作都是串行执行。


泊浮目
4.9k 声望1.3k 粉丝