深刻淺出Zookeeper(五):BadVersionException究竟是怎麼一回事

本文首發於 泊浮目的簡書: https://www.jianshu.com/u/204...
版本 日期 備註
1.0 2020.4.19 文章首發

前言

最近在開發時偶爾會觀測到zk報出BadVersionException,後在搜索引發上得知了是樂觀鎖相關的問題,很快就解決了問題。不過學而不思則罔:不管是單體應用仍是分佈式系統,在運行過程當中總要有一種機制來保證數據排他性。接下來,咱們就來看看zk是如何實現這種機制的。java

節點屬性

在此分析源碼以前,咱們須要瞭解zk節點的三種版本屬性:node

  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代碼很是簡單:session

/**
     * 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裏的代碼:async

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典型使用場景數據併發競爭的狀況較少(固然,你可讓它競爭很激烈,只是總體來看過程會變得較爲耗時),且事務操做都是串行執行。

相關文章
相關標籤/搜索