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

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
简介: 最近在开发时偶尔会观测到zk报出`BadVersionException`,后在搜索引起上得知了是乐观锁相关的问题,很快就解决了问题。不过学而不思则罔:无论是单体应用还是分布式系统,在运行过程中总要有一种**机制**来保证数据排他性。接下来,我们就来看看zk是如何实现这种**机制**的。
版本 日期 备注
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,代码会这么写:

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

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
目录
相关文章
|
7月前
|
存储 负载均衡 算法
深入浅出Zookeeper源码(七):Leader选举
对于一个分布式集群来说,保证数据写入一致性最简单的方式就是依靠一个节点来调度和管理其他节点。在分布式系统中我们一般称其为Leader。
205 6
|
5月前
|
API
【想进大厂还不会阅读源码】ShenYu源码-替换ZooKeeper客户端
ShenYu源码阅读。相信大家碰到源码时经常无从下手,不知道从哪开始阅读😭。我认为有一种办法可以解决大家的困扰!至此,我们发现自己开始从大量堆砌的源码中脱离开来😀。ShenYu是一个异步的,高性能的,跨语言的,响应式的 API 网关。
|
7月前
|
存储 设计模式 算法
深入浅出Zookeeper源码(六):客户端的请求在服务器中经历了什么
当我们向zk发出一个数据更新请求时,这个请求的处理流程是什么样的?zk又是使用了什么共识算法来保证一致性呢?带着这些问题,我们进入今天的正文。
183 1
深入浅出Zookeeper源码(六):客户端的请求在服务器中经历了什么
|
7月前
|
Apache
Apache ZooKeeper - 构建ZooKeeper源码环境及StandAlone模式下的服务端和客户端启动
Apache ZooKeeper - 构建ZooKeeper源码环境及StandAlone模式下的服务端和客户端启动
135 2
|
7月前
|
网络协议 数据库
深入浅出Zookeeper源码(三):会话管理
我们知道zookeeper是一个分布式协同系统。在一个大型的分布式系统中,必然会有大量的client来连接zookeeper。那么zookeeper是如何管理这些session的生命周期呢?带着这个问题,我们进入今天的正文。
127 1
|
7月前
|
存储 设计模式 算法
深入浅出Zookeeper源码(六):客户端的请求在服务器中经历了什么
当我们向zk发出一个数据更新请求时,这个请求的处理流程是什么样的?zk又是使用了什么共识算法来保证一致性呢?带着这些问题,我们进入今天的正文。
137 0
|
7月前
|
Java API 开发者
深入浅出Zookeeper源码(四):Watch实现剖析
用过zookeeper的同学都知道watch是一个非常好用的机制,今天我们就来看看它的实现原理。 在正文开始前,我们先来简单回忆一下watch是什么? zk提供了分布式数据的发布/订阅功能——即典型的发布订阅模型,其定义了一种一对多的订阅关系,能够让多个订阅者同时监听某个主题对象,当这个主题对象自身状态变化时,则会通知所有订阅者。具体来说,则是zk允许一个客户端向服务端注册一个watch监听,当服务端的一些指定事件触发了这个watch,那么就会向该客户端发送事件通知。
113 0
|
7月前
|
存储 关系型数据库 MySQL
深入浅出Zookeeper源码(二):存储技术
在上篇文章中,我们简单提到了Zookeeper的几个核心点。在这篇文章中,我们就来探索其存储技术。在开始前,读者可以考虑思考下列问题: - Zookeeper的数据存储是如何实现的? - Zookeeper进行一次写操作的时候,会发生什么å? - 当一个Zookeeper新加入现有集群时,如何同步现集群中的数据?
91 0
|
7月前
|
消息中间件 Java Shell
Linux【脚本 03】shell脚本离线安装配置集结JDK+InfluxDB+Zookeeper+Kafka(安装文件及脚本源码网盘分享)
Linux【脚本 03】shell脚本离线安装配置集结JDK+InfluxDB+Zookeeper+Kafka(安装文件及脚本源码网盘分享)
65 0
|
消息中间件 算法 网络协议
50-微服务技术栈(高级):分布式协调服务zookeeper源码篇(Leader选举)
前面学习了Zookeeper相关细节,其中对于集群启动而言,很重要的一部分就是Leader选举,接着就开始深入学习Leader选举。
129 0