正確解鎖分布式鎖的各種姿勢

分布式鎖

為什么要用分布式鎖?
在分布式場景下多個客戶端同時獲取一把鎖,為了保證只有一個客戶端能獲取到這把鎖,分布式鎖誕生了,而分布式鎖的誕生就是為了解決數(shù)據(jù)的最終一致性.在分布式系統(tǒng)中有一個著名的原理叫做CAP原理:其中Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區(qū)容錯性),它描述的是在分布式系統(tǒng)中同時最多只能滿足其中兩個條件,如果你想實現(xiàn)強一致性你就需要犧牲高性能.如果服務是單點,那其實本質(zhì)上就不存在分布式鎖這個概念.

本文接下來將探討實現(xiàn)分布式鎖的幾種解決方案,并做下對比

1. 說明

??關(guān)于分布式鎖這塊,其實沒有辦法做到100%的絕對安全性

2. 背景

??在很多秒殺系統(tǒng)中、生成全局唯一遞增id、支付、任務分配等場景都需要用到分布式鎖

3. 數(shù)據(jù)庫樂觀鎖

??基于數(shù)據(jù)庫樂觀鎖實現(xiàn)分布式鎖,講到樂觀鎖順便講一下悲觀鎖
悲觀鎖:悲觀鎖認為所有的操作都是不安全的,所以操作之前它先上鎖,比如行鎖、表鎖都是基于悲觀鎖原理實現(xiàn)的
樂觀鎖:樂觀鎖和悲觀鎖不同的是,只在數(shù)據(jù)更新的做比較,查詢階段不加鎖,所以性能上要快一些
樂觀鎖的實現(xiàn)大多是基于數(shù)據(jù)庫的版本號機制去實現(xiàn)的,簡單的來說就是,即為數(shù)據(jù)增加一個版本標識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表添加一個 “version”字段來實現(xiàn)讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加1,其中id是唯一建.
假設(shè)數(shù)據(jù)庫有這樣一條記錄:

id name version
1 jack 100

大致的過程就是:
這個時候假設(shè)有兩個線程(c1,c2)同時要對id為1的記錄進行修改
1)先查詢出來id為1的記錄
2)那么兩個線程查詢出來的結(jié)果都是一樣的,c1想把id為1的name修改成tom,c2想把id為1的name修改成robin
3)c1在更新的時候,c2也在更新記錄,假設(shè)c2后更新,更新的時候兩者都帶上最近一次查詢的版本號
4)如果發(fā)現(xiàn)更新失敗,重試,說明此記錄已經(jīng)被修改

這個實現(xiàn)看看就行了

4. 基于Redis分布式鎖實現(xiàn)

??redis分布式鎖是目前很多大公司采取的技術(shù)解決方案,基于Redis單線程模式,采用隊列模式,隊列本是就是先進后出的模型,將并發(fā)訪問轉(zhuǎn)變成串行訪問,這樣就避免了資源的有序性和競爭關(guān)系.

早期的時候我們用的比較多的是setNX命令,做一些簡單的操作,偽代碼如下:

    long result= JedisUtil.setnx(key,value);
    if(result>0)  {
    JedisUtil.expire(key,locktime);
    return true;
    }  
    long expire=Jedisutil.ttl(key);
    if(expire<0){
    if(expire==-1{
    JedisUtil.remove(key);
    ....
    }
    }

這段代碼看似沒有什么毛病,一般情況下也確實問題不大,但是實現(xiàn)上并不是安全的.

1)第一種情況:執(zhí)行expire命令時候發(fā)生了網(wǎng)絡抖動,執(zhí)行超時或者失敗了,如果這把鎖不釋放,它將會成為死鎖.(我的想法比較~發(fā)生的概率比較小)

2)第二種情況:因為現(xiàn)在的操作是非原子性的,那么理論上說你的操作都可以被“打斷”,比如你的redis部署采用哨兵集群模式,簡單了來說就是HA高可用,主從模式,其實嚴格來說就不是集群了,如果主節(jié)點掛了,由于是主從異步復制的,這個時候從還沒同步過去,就導致主有這個key而從沒有key,被提升為主的從節(jié)點會導致在某個時刻,會有多個客戶端同時持有該key.這個在后續(xù)的redlock和redission中都有解決.

3)第三種情況:超時時間如果控制的不但,會出現(xiàn),任務還沒執(zhí)行完,到了超時時間,這個時候鎖自動釋放了,這個就尷尬了,此時你就要解決自動續(xù)租的問題了

4)效率比較低,強依賴于失效時間,需要一直去輪詢鎖是否失效

redis官方推薦我們配合lua腳本去解決原子性問題:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(fullKey), Collections.singletonList(value));
            if (Objects.equals(UNLOCK_SUCCESS, result)) {
                flag = true;
            }

redission實現(xiàn)分布式鎖

樓主比較推薦: 因為簡單易用,功能強大

相比Jedis而言可伸縮性性更高,jedis使用的是阻塞IO,而Redisson使用非阻塞的I/O和基于Netty框架的事件驅(qū)動的通信層,而且Redisson的API是線程安全的,這個就比較討人喜歡了.

支持更豐富的數(shù)據(jù)結(jié)構(gòu)比如:BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service

引入maven依賴:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>2.2.13</version>
</dependency>

代碼實現(xiàn):

RLock lock = redisson.getLock("lockName");
try{
    //  嘗試加鎖,最多等待2秒,上鎖以后8秒自動解鎖
    boolean res = lock.tryLock(2, 8, TimeUnit.SECONDS);
    if(res){ //成功
        //處理業(yè)務
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    //釋放鎖
    lock.unlock();
}

5. 基于zk開源客戶端Curator實現(xiàn)分布式鎖

Curator內(nèi)部是通過InterProcessMutex(可重入鎖)來在zookeeper中創(chuàng)建臨時有序節(jié)點實現(xiàn)的.基于zab協(xié)議保證鎖的數(shù)據(jù)安全性,基于ping的心跳?;顧C制解決死鎖問題,并通過watch機制能及時喚醒被阻塞的狀態(tài)

1). 基于Zk名稱唯一性

利用名稱唯一性,加鎖操作就是建立一個znode節(jié)點,釋放鎖就是刪除這個目錄,操作簡單,ZAB一致性協(xié)議保證了鎖的數(shù)據(jù)安全性。缺點就是會產(chǎn)生著名的“羊群”效應,N個客戶端在等待一把鎖時,鎖釋放時候所有客戶端都被喚醒,而僅僅只有一個客戶端才能得到鎖。

2). 基于臨時有序順序節(jié)點

首先建立一個節(jié)點;
當進程訪問資源時,獲得鎖,創(chuàng)建znode節(jié)點下順序節(jié)點;
對znode節(jié)點下子節(jié)點排序,序號最小的獲得鎖;后面的節(jié)點獲得上一順序節(jié)點,并注冊監(jiān)聽事件,等待監(jiān)聽事件,獲得鎖;資源用完后釋放資源,調(diào)用unlock,去關(guān)掉zk

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
    try 
    {
        
    } finally {
        lock.release();
    }
}

基于zk實現(xiàn)的分布式鎖,安全可靠,也不需要考慮沒有設(shè)置失效時間的問題,因為zk會自動刪除znode節(jié)點目錄,但是zk相對redis效率比較低,因為他會頻繁的創(chuàng)建和銷毀節(jié)點,性能上不是很好,所以不太適合并發(fā)比較高的業(yè)務場景.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容