分佈式鎖選型背後的架構思維(帶源碼)

此文爲林淮川(鄙人)和玄姐(轉轉首席架構師)共同編寫,發佈在csdn和 架構之美工作號上。
https://blog.csdn.net/jianxian89/article/details/103584137 轉載請保留!

1. 分佈式鎖本質

提到分佈式鎖,有很多實現,比如Redis分佈式鎖、ZooKeeper分佈式鎖、etcd分佈式鎖等。但是選擇哪個更適合你的項目?在《基於CAP模型設計企業級真正高可用的分佈式鎖》一文深入分析過分佈式鎖的哲學本質,以及如何結合場景來選擇合適的分佈式鎖。分析業務場景,得到業務本質,就是架構思維。思維最終是需要落地的,接下去分享一下對分佈式鎖的思考和實踐。。

鎖的本質是對共享資源的處理,表現很多,有以下作用:

  1. 業務協調
  2. 業務冪等(需配合業務代碼實現)
  3. 共享資源競爭

在單體應用時代表現爲的就是同步塊,lock。隨着需求和業務量的增長,系統走向了分佈式、微服務時代,多服務/多實例下的應用無法使用本地鎖進行控制資源共享。這時就出現了分佈式鎖,我分析了分佈式場景下的情況,可以歸納出分佈式鎖的要求:

  1. 強一致性
  2. 服務高可用、系統穩健
  3. 鎖自動續約、自動釋放
  4. 業務可重入

2. 分佈式鎖存儲選型及場

目前網上常見分佈式鎖的實現有redis、zookeeper、etcd,先來個總體比較。

  redis zookeeper etcd
一致性算法 ZAB(paxos自有實現) raft
CAP AP CP CP
高可用 主從 N+1可用 N+1可用
實現 setNX createEphemeral http/grpc

      接下來,咱們對比較指標進行一一解釋:

  1. 一致性算法(CAP):在分佈式場景下,CAP理論是很多設計的指導思想。CAP思想下有兩個分支,cp與ap;cp:不管啥情況下,都要求各服務之間的數據一致;AP:高可用下的數據最終一致性。雖然鎖原本要求強一致性,但基於CAP理論也對AP有了一定的容忍度。AP分佈式鎖的使用取決於 業務場景對 髒數據的最大容忍度,比如SNS場景,這時AP鎖存在,從性能上有很大的優勢。CP仍然保持原有的一致性要求,保證了業務資源串行競爭,更加適合於金融交易場景的強數據要求。redis自身無一致性算法來保證多節點的數據一致性,所以是AP模式;zk、etcd都有一致性算法,所以都是CP模式。
  2. 高可用:redis是一個k-v的存儲;使用主從模式進行集羣,redis-cluster 底層也是主從模式的組合;性能高,也保證了靠可用。zk是tree的數據結構,節點要求N+1,N必須大於2;通過 ZAB選舉 保障主的可用。etcd是一個k-v的存儲,節點要求N+1,N必須大於2;通過 raft選舉 保障主的可用存在。

3. 分佈式鎖接口設

根據要求,可以設計出鎖接口,首先鎖的基本方法:

/**
     * @方法名稱 lock
     * @功能描述 <pre>獲取鎖</pre>
     * @param ttl 鎖過期時間,單位毫秒
     * @return true-獲取鎖,false-未獲得鎖
     * @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
     */
    boolean lock(int ttl)
        throws RuntimeException;

 因爲分佈式鎖可以處理業務冪等,可用作爲消息去重等場景,所以設計競爭鎖方法

    /**
     * @方法名稱 acquire
     * @功能描述 <pre>競爭鎖,並自動續租</pre>
     * @param ttl 鎖過期時間,單位毫秒
     * @return true-獲取鎖,false-未獲得鎖
     * @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
     */
    default boolean acquire(int ttl)
        throws RuntimeException {
        if (lock(ttl)) {
            logger.debug(MSG_LOCK, getName());
            startHeartBeatThread();
            return true;
        }
        return false;
    }

其次作爲鎖的基本要求,業務的串行執行,所以設計等待鎖方法

 /**
     * @方法名稱 acquireOrWait
     * @功能描述 <pre>競爭鎖或等待鎖</pre>
     * @param ttl 鎖過期時間,單位毫秒
     * @param waitTime 等待時間,單位毫秒
     * @return true-獲取鎖,false-未獲得鎖
     * @throws InterruptedException
     * @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
     */
    default boolean acquireOrWait(int ttl, int waitTime)
        throws InterruptedException, RuntimeException {
        while (!lock(ttl)) {
            waitTime = waitTime - ttl / 2;
            Thread.sleep(ttl / 2);
            if (waitTime <= 0) {
                logger.debug(MSG_LOCK_TIMEOUT, getName());
                return false;
            }
        }
        startHeartBeatThread();
        return true;
    }
   

因爲是分佈式場景,所以鎖需要存在自動續租方法,保障鎖內業務的完整執行。

/**
     * @方法名稱 startHeartBeatThread
     * @功能描述 <pre>續租心跳</pre>
     */
    void startHeartBeatThread(); 

最後,鎖需要自動釋放,爲保證使用簡單,所以重寫Closeable接口:

   
    /**
     * @方法名稱 close
     * @功能描述 <pre>釋放鎖</pre>
     */
    @Override
    void close();
    
    /**
     * @方法名稱 release
     * @功能描述 <pre>釋放鎖</pre>
     */
    default void release() {
        close();
    }

4. AP模型的Redis分佈式鎖實現

    /**
     * 加鎖腳本
     */
    private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
    
    /**
     * 解鎖腳本
     */
    private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * 加鎖腳本sha1值
     */
    private static final String SCRIPT_LOCK_SHA1 = "8525317db2b346fffb9050797aee5fa8b8231872";
    
    /**
     * 解鎖腳本sha1值
     */
    private static final String SCRIPT_UNLOCK_SHA1 = "e9f69f2beb755be68b5e456ee2ce9aadfbc4ebf4";

    @Override
    public boolean lock(int ttl)
        throws RuntimeException {
        this.ttl = ttl;
        // 鎖不存在時:上鎖並過期時間,最後跳出。
        Long result = redisTemplate.execute(new RedisScript<Long>() {
            @Override
            public String getSha1() {
                return SCRIPT_LOCK_SHA1;
            }
            @Override
            public Class<Long> getResultType() {
                return Long.class;
            }
            @Override
            public String getScriptAsString() {
                return SCRIPT_LOCK;
            }
        }, Collections.singletonList(lockKey), "1", String.valueOf(ttl));
        if (SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
    @Override
    public void startHeartBeatThread() {
        scheduledpool = new ScheduledThreadPoolExecutor(1, new NamingThreadFactory(Thread.currentThread().getName().concat(LOCK_HEART_BEAT), true));
        scheduledpool.scheduleAtFixedRate(() -> {
            redisTemplate.expire(lockKey, ttl, TimeUnit.MILLISECONDS);
            logger.debug(MSG_RELET, Thread.currentThread().getName(), lockKey);
        }, 0L, ttl / 3, TimeUnit.MILLISECONDS);
        
    }
    @Override
    public void close() {
        if (null != scheduledpool && redisTemplate.hasKey(lockKey)) {
            try {
                redisTemplate.execute(new RedisScript<Long>() {
                    @Override
                    public Class<Long> getResultType() {
                        return Long.class;
                    }
                    
                    @Override
                    public String getScriptAsString() {
                        return SCRIPT_UNLOCK;
                    }
                    
                    @Override
                    public String getSha1() {
                        return SCRIPT_UNLOCK_SHA1;
                    }
                }, Collections.singletonList(lockKey), "1");
            } catch (Exception e) {
                logger.warn(WARN_MSG_EVAL, e.getMessage());
                redisTemplate.delete(lockKey);
            }
        }
    }

5. CP模型的etcd分佈式鎖實現

etcd有V2和V3兩種接口:v2接口可以使用http直接訪問,天然客戶端物理解耦,但需要自動續租保證鎖的完整性。v3接口默認grpc形式,是長鏈接機制,天然續租,但grpc有客戶端依賴要求。可以根據場景要求,適度選擇合適版本接口。

 鎖參數有:

            a、prevExits
           檢查是否存在,true--新增,false--更新

            b、prevIndex
           檢查上一個的key,既操作返回的uuid
           
            c、prevValue
           檢查上一個的值

 

curl鎖操作
            a、取鎖
           curl  http://ip:port/v2/keys/鎖名 -XPUT -d ttl=10 -d prevExits=false -d value=鎖值

            b、續租
           curl  http://ip:port/v2/keys/鎖名?prevValue=鎖值 -XPUT -d ttl=3 -d prevExits=true -d refresh=true

           通過心跳線程,每隔 ttl/3時間,更新過期時間
       
            c、放鎖
           curl  http://ip:port/v2/keys/鎖名?prevValue=鎖值 -XDELETE

 

6. CP模型的zk分佈式公平重入鎖

使用zk 的臨時順序節點,可以充當競鎖隊列,保障鎖的公平性,源碼基於原生zk-client進行實現。用

7. 源碼

https://github.com/linhuaichuan/ecp-uid/tree/master/src/test/java/com/myzmds/ecp/core/standard/distributed/lock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章