前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入浅出Zookeeper源码(五):BadVersionException到底是怎么一回事

深入浅出Zookeeper源码(五):BadVersionException到底是怎么一回事

作者头像
泊浮目
发布2024-01-09 13:41:25
1090
发布2024-01-09 13:41:25
举报
文章被收录于专栏:狗哥的专栏狗哥的专栏

版本

日期

备注

1.0

2020.4.19

文章首发

1.1

2021.6.23

标题从深入浅出Zookeeper(五):BadVersionException到底是怎么一回事改变为深入浅出Zookeeper源码(五):BadVersionException到底是怎么一回事

前言

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

节点属性

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

  1. version: 当前数据节点数据内容的版本号
  2. cversion: 当前数据子节点的版本号
  3. aversion: 当前数据节点ACL变更版本号

这些属性都可以在StatPersisted这个类里找到。

当相关的属性进行变更时,版本号则会+1。刚创建的节点,版本号为0,表示这个节点被更新过0次。

源码分析

一般如果我们调用setData,代码会这么写:

代码语言:javascript
复制
//Curator版本
//要求版本对比。当然,填-1服务端在接收时便不会去对比了
client.setData().withVersion(version).forPath(path, payload);
//不要求版本对比
client.setData().forPath(path, payload);

zookeeper的client代码非常简单:

代码语言:javascript
复制
    /**
     * 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里的代码:

代码语言:javascript
复制
       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的逻辑:

代码语言:javascript
复制
    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典型使用场景数据并发竞争的情况较少(当然,你可以让它竞争很激烈,只是整体来看过程会变得较为耗时),且事务操作都是串行执行。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 节点属性
  • 源码分析
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档