一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

這幾種常見(jiàn)的“分布式鎖”寫(xiě)法,搞懂再也不怕面試官,安排

 紫火神兵 2020-09-11

什么是分布式鎖?

大家好,我是jack xu,今天跟大家聊一聊分布式鎖。首先說(shuō)下什么是分布式鎖,當(dāng)我們?cè)谶M(jìn)行下訂單減庫(kù)存,搶票,選課,搶紅包這些業(yè)務(wù)場(chǎng)景時(shí),如果在此處沒(méi)有鎖的控制,會(huì)導(dǎo)致很?chē)?yán)重的問(wèn)題。學(xué)過(guò)多線程的小伙們知道,為了防止多個(gè)線程同時(shí)執(zhí)行同一段代碼,我們可以用 synchronized 關(guān)鍵字或 JUC 里面的 ReentrantLock 類(lèi)來(lái)控制,但是目前幾乎任何一個(gè)系統(tǒng)都是部署多臺(tái)機(jī)器的,單機(jī)部署的應(yīng)用很少,synchronized 和 ReentrantLock 發(fā)揮不出任何作用,此時(shí)就需要一把全局的鎖,來(lái)代替 JAVA 中的 synchronized 和 ReentrantLock。

分布式鎖的實(shí)現(xiàn)方式流行的主要有三種,分別是基于緩存 Redis 的實(shí)現(xiàn)方式,基于 zk 臨時(shí)順序節(jié)點(diǎn)的實(shí)現(xiàn)以及基于數(shù)據(jù)庫(kù)行鎖的實(shí)現(xiàn)。我們先來(lái)說(shuō)下用 Jedis 中的 setnx 命令來(lái)構(gòu)建這把鎖。

Jedis寫(xiě)法

使用 Redis 做分布式鎖的思路是,在 redis 中設(shè)置一個(gè)值表示加了鎖,然后釋放鎖的時(shí)候就把這個(gè) key 刪除。思路是很簡(jiǎn)單,但是在使用過(guò)程中要避免一些坑,我們先看下加鎖的代碼:

    /**
     * 嘗試獲取分布式鎖
     *
     * @param jedis      Redis客戶(hù)端
     * @param lockKey    鎖
     * @param requestId  請(qǐng)求標(biāo)識(shí)
     * @param expireTime 超期時(shí)間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多個(gè)參數(shù) NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

這段代碼很簡(jiǎn)單,主要說(shuō)下這里用的命令是 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL],而沒(méi)有使用 SETNX+EXPIRE 的命令,原因是 SETNX+EXPIRE 是兩條命令無(wú)法保證原子性,而 SET 是原子操作。那這里為什么要設(shè)置超時(shí)時(shí)間呢?原因是當(dāng)一個(gè)客戶(hù)端獲得了鎖在執(zhí)行任務(wù)的過(guò)程中掛掉了,來(lái)不及顯式地釋放鎖,這塊資源將會(huì)永遠(yuǎn)被鎖住,這將會(huì)導(dǎo)致死鎖,所以必須設(shè)置一個(gè)超時(shí)時(shí)間。

釋放鎖的代碼如下:

    /**
     * 釋放分布式鎖
     *
     * @param jedis     Redis客戶(hù)端
     * @param lockKey   鎖
     * @param requestId 請(qǐng)求標(biāo)識(shí),當(dāng)前工作線程線程的名稱(chēng)
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        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(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

這里也有兩點(diǎn)注意的地方,第一是解鈴還須系鈴人,怎么理解呢,就是 A 加的鎖 B 不能去 del 掉吧,不然豈不是全亂套了,誰(shuí)加的鎖就誰(shuí)去解,我們一般把 value 設(shè)為當(dāng)前線程的 Id,Thread.currentThread().getId(),然后在刪的時(shí)候判斷下是不是當(dāng)前線程。第二點(diǎn)是驗(yàn)證和釋放鎖是兩個(gè)獨(dú)立操作,不是原子性,這個(gè)怎么解決呢?使用 Lua 腳本,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能給我們保證原子性。

Redisson寫(xiě)法

Redisson 是 Java 的 Redis 客戶(hù)端之一,提供了一些 API 方便操作 Redis。但是 Redisson 這個(gè)客戶(hù)端可有點(diǎn)厲害,我們先打開(kāi)官網(wǎng)看下 github.com/redisson/re…

image

這個(gè)目錄里面有很多的功能,Redisson 跟 Jedis 定位不同,它不是一個(gè)單純的 Redis 客戶(hù)端,而是基于 Redis 實(shí)現(xiàn)的分布式的服務(wù),我們可以看到還有 JUC 包下面的類(lèi)名,Redisson 幫我們搞了分布式的版本,比如 AtomicLong,直接用 RedissonAtomicLong 就行了。鎖只是它的冰山一角,并且它對(duì)主從,哨兵,集群等模式都支持,當(dāng)然了,單節(jié)點(diǎn)模式肯定是支持的。

在 Redisson 里面提供了更加簡(jiǎn)單的分布式鎖的實(shí)現(xiàn),我們來(lái)看下它的用法,相當(dāng)?shù)暮?jiǎn)單,兩行代碼搞定,比 Jedis 要簡(jiǎn)單的多,而且在 Jedis 里需要考慮的問(wèn)題,它都已經(jīng)幫我們封裝好了。

image

我們來(lái)看下,這里獲取鎖有很多種的方式,有公平鎖有讀寫(xiě)鎖,我們使用的是 redissonClient.getLock, 這是一個(gè)可重入鎖。

image

現(xiàn)在我把程序啟動(dòng)一下

image

打開(kāi) Redis Desktop Manager 工具,看下到底它存的是什么。原來(lái)在加鎖的時(shí)候,寫(xiě)入了一個(gè) HASH 類(lèi)型的值,key 是鎖名稱(chēng) jackxu,field 是線程的名稱(chēng),而 value 是 1(即表示鎖的重入次數(shù))。

image

小伙伴可能覺(jué)得我在一派胡言,沒(méi)關(guān)系,我們點(diǎn)進(jìn)去看下它的源碼是具體實(shí)現(xiàn)的。

image

點(diǎn)進(jìn) tryLock() 方法的 tryAcquire() 方法,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),終于見(jiàn)到廬山真面目了,原來(lái)它最終也是通過(guò) Lua 腳本來(lái)實(shí)現(xiàn)的。

image

現(xiàn)在我把這段Lua腳本拿出來(lái)分析一下,很簡(jiǎn)單。

// KEYS[1] 鎖名稱(chēng) updateAccount
// ARGV[1] key 過(guò)期時(shí)間 10000ms
// ARGV[2] 線程名稱(chēng)
// 鎖名稱(chēng)不存在
if (redis.call('exists', KEYS[1]) == 0) then
// 創(chuàng)建一個(gè) hash,key=鎖名稱(chēng),field=線程名,value=1
redis.call('hset', KEYS[1], ARGV[2], 1);
// 設(shè)置 hash 的過(guò)期時(shí)間
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 鎖名稱(chēng)存在,判斷是否當(dāng)前線程持有的鎖
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 如果是,value+1,代表重入次數(shù)+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重新獲得鎖,需要重新設(shè)置 Key 的過(guò)期時(shí)間
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 鎖存在,但是不是當(dāng)前線程持有,返回過(guò)期時(shí)間(毫秒)
return redis.call('pttl', KEYS[1]);

unlock() 中的 unlockInnerAsync() 釋放鎖,同樣也是通過(guò) Lua 腳本實(shí)現(xiàn)。

// KEYS[1] 鎖的名稱(chēng) updateAccount
// KEYS[2] 頻道名稱(chēng) redisson_lock__channel:{updateAccount}
// ARGV[1] 釋放鎖的消息 0
// ARGV[2] 鎖釋放時(shí)間 10000
// ARGV[3] 線程名稱(chēng)
// 鎖不存在(過(guò)期或者已經(jīng)釋放了)
if (redis.call('exists', KEYS[1]) == 0) then
// 發(fā)布鎖已經(jīng)釋放的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 鎖存在,但是不是當(dāng)前線程加的鎖
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;

// 鎖存在,是當(dāng)前線程加的鎖
// 重入次數(shù)-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
// -1 后大于 0,說(shuō)明這個(gè)線程持有這把鎖還有其他的任務(wù)需要執(zhí)行
if (counter > 0) then
// 重新設(shè)置鎖的過(guò)期時(shí)間
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// -1 之后等于 0,現(xiàn)在可以刪除鎖了
redis.call('del', KEYS[1]);
// 刪除之后發(fā)布釋放鎖的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;

// 其他情況返回 nil
return nil;

看完它的使用后,我們發(fā)現(xiàn)真的使用起來(lái)像 JDK 中的 ReentrantLock 一樣絲滑。

image

RedLock

RedLock 的中文是直譯過(guò)來(lái)的,就叫紅鎖。紅鎖并非是一個(gè)工具,而是 Redis 官方提出的一種分布式鎖的算法。我們知道如果采用單機(jī)部署模式,會(huì)存在單點(diǎn)問(wèn)題,只要 redis 故障了,加鎖就不行了。如果采用 master-slave 模式,加鎖的時(shí)候只對(duì)一個(gè)節(jié)點(diǎn)加鎖,即便通過(guò) sentinel 做了高可用,但是如果 master 節(jié)點(diǎn)故障了,發(fā)生主從切換,此時(shí)就會(huì)有可能出現(xiàn)鎖丟失的問(wèn)題?;谝陨系目紤],其實(shí) redis 的作者 Antirez 也考慮到這個(gè)問(wèn)題,他提出了一個(gè) RedLock 的算法。

我在這里畫(huà)了一個(gè)圖,圖中這五個(gè)實(shí)例都是獨(dú)自部署的,沒(méi)有主從關(guān)系,它們就是5個(gè) master 節(jié)點(diǎn)。

image

通過(guò)以下步驟獲取一把鎖:

  • 獲取當(dāng)前時(shí)間戳,單位是毫秒
  • 輪流嘗試在每個(gè) master 節(jié)點(diǎn)上創(chuàng)建鎖,過(guò)期時(shí)間設(shè)置較短,一般就幾十毫秒
  • 嘗試在大多數(shù)節(jié)點(diǎn)上建立一個(gè)鎖,比如5個(gè)節(jié)點(diǎn)就要求是3個(gè)節(jié)點(diǎn)(n / 2 +1)
  • 客戶(hù)端計(jì)算建立好鎖的時(shí)間,如果建立鎖的時(shí)間小于超時(shí)時(shí)間,就算建立成功了
  • 要是鎖建立失敗了,那么就依次刪除這個(gè)鎖
  • 只要?jiǎng)e人建立了一把分布式鎖,你就得不斷輪詢(xún)?nèi)L試獲取鎖

但是這樣的這種算法還是頗具爭(zhēng)議的,可能還會(huì)存在不少的問(wèn)題,無(wú)法保證加鎖的過(guò)程一定正確。Martin Kleppmann 針對(duì)這個(gè)算法提出了質(zhì)疑,接著 antirez 又回復(fù)了 Martin Kleppmann 的質(zhì)疑。一個(gè)是很有資歷的分布式架構(gòu)師,一個(gè)是 Redis 之父,這個(gè)就是著名的關(guān)于紅鎖的神仙打架事件。

最后 Redisson 官網(wǎng)上也給出了如何使用紅鎖 redlock,幾行代碼搞定,依然很絲滑,感興趣的小伙伴可以看下。

image

Zookeeper寫(xiě)法

在介紹 zookeeper 實(shí)現(xiàn)分布式鎖的機(jī)制之前,先粗略介紹一下 zk 是什么東西: zk 是一種提供配置管理、分布式協(xié)同以及命名的中心化服務(wù)。它的模型是這樣的:包含一系列的節(jié)點(diǎn),叫做znode,就好像文件系統(tǒng)一樣每個(gè) znode 表示一個(gè)目錄,然后 znode 有一些特性,我們可以把它們分為四類(lèi):

  • 持久化節(jié)點(diǎn)(zk斷開(kāi)節(jié)點(diǎn)還在)
  • 持久化順序節(jié)點(diǎn)(如果是第一個(gè)創(chuàng)建的子節(jié)點(diǎn),那么生成的子節(jié)點(diǎn)為/lock/node-0000000000,下一個(gè)節(jié)點(diǎn)則為/lock/node-0000000001,依次類(lèi)推)
  • 臨時(shí)節(jié)點(diǎn)(客戶(hù)端斷開(kāi)后節(jié)點(diǎn)就刪除了)
  • 臨時(shí)順序節(jié)點(diǎn)

zookeeper分布式鎖恰恰應(yīng)用了臨時(shí)順序節(jié)點(diǎn),下面我們就用圖解的方式來(lái)看下是怎么實(shí)現(xiàn)的。

獲取鎖

首先,在 Zookeeper 當(dāng)中創(chuàng)建一個(gè)持久節(jié)點(diǎn) ParentLock。當(dāng)?shù)谝粋€(gè)客戶(hù)端想要獲得鎖時(shí),需要在ParentLock這個(gè)節(jié)點(diǎn)下面創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn) Lock1。

image

之后,Client1 查找 ParentLock 下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn) Lock1 是不是順序最靠前的一個(gè)。如果是第一個(gè)節(jié)點(diǎn),則成功獲得鎖。

image

這時(shí)候,如果再有一個(gè)客戶(hù)端 Client2 前來(lái)獲取鎖,則在 ParentLock 下再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock2。

image

Client2 查找 ParentLock 下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn) Lock2 是不是順序最靠前的一個(gè),結(jié)果發(fā)現(xiàn)節(jié)點(diǎn) Lock2 并不是最小的。

于是,Client2 向排序僅比它靠前的節(jié)點(diǎn) Lock1 注冊(cè) Watcher,用于監(jiān)聽(tīng) Lock1 節(jié)點(diǎn)是否存在。這意味著 Client2 搶鎖失敗,進(jìn)入了等待狀態(tài)。

image

這時(shí)候,如果又有一個(gè)客戶(hù)端 Client3 前來(lái)獲取鎖,則在ParentLock下載再創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)Lock3。

image

Client3 查找 ParentLock 下面所有的臨時(shí)順序節(jié)點(diǎn)并排序,判斷自己所創(chuàng)建的節(jié)點(diǎn) Lock3 是不是順序最靠前的一個(gè),結(jié)果同樣發(fā)現(xiàn)節(jié)點(diǎn) Lock3 并不是最小的。

于是,Client3 向排序僅比它靠前的節(jié)點(diǎn) Lock2 注冊(cè) Watcher,用于監(jiān)聽(tīng) Lock2 節(jié)點(diǎn)是否存在。這意味著 Client3 同樣搶鎖失敗,進(jìn)入了等待狀態(tài)。

image

這樣一來(lái),Client1 得到了鎖,Client2 監(jiān)聽(tīng)了 Lock1,Client3 監(jiān)聽(tīng)了 Lock2。這恰恰形成了一個(gè)等待隊(duì)列,很像是 Java 當(dāng)中 ReentrantLock 所依賴(lài)的 AQS(AbstractQueuedSynchronizer)。

釋放鎖

釋放鎖分為兩種情況:

1.任務(wù)完成,客戶(hù)端顯示釋放

當(dāng)任務(wù)完成時(shí),Client1 會(huì)顯示調(diào)用刪除節(jié)點(diǎn) Lock1 的指令。

image

2.任務(wù)執(zhí)行過(guò)程中,客戶(hù)端崩潰

獲得鎖的 Client1 在任務(wù)執(zhí)行過(guò)程中,如果 Duang 的一聲崩潰,則會(huì)斷開(kāi)與 Zookeeper 服務(wù)端的鏈接。根據(jù)臨時(shí)節(jié)點(diǎn)的特性,相關(guān)聯(lián)的節(jié)點(diǎn) Lock1 會(huì)隨之自動(dòng)刪除。

image

由于 Client2 一直監(jiān)聽(tīng)著 Lock1 的存在狀態(tài),當(dāng) Lock1 節(jié)點(diǎn)被刪除,Client2 會(huì)立刻收到通知。這時(shí)候 Client2 會(huì)再次查詢(xún) ParentLock 下面的所有節(jié)點(diǎn),確認(rèn)自己創(chuàng)建的節(jié)點(diǎn) Lock2 是不是目前最小的節(jié)點(diǎn)。如果是最小,則 Client2 順理成章獲得了鎖。

image

同理,如果 Client2 也因?yàn)槿蝿?wù)完成或者節(jié)點(diǎn)崩潰而刪除了節(jié)點(diǎn) Lock2,那么 Client3 就會(huì)接到通知。

image

最終,Client3 成功得到了鎖。

image

Curator

在 Apache 的開(kāi)源框架 Apache Curator 中,包含了對(duì) Zookeeper 分布式鎖的實(shí)現(xiàn)。 github.com/apache/cura…

它的使用方式也很簡(jiǎn)單,如下所示:

image

我們看了下依然絲滑,源碼我就不分析了,感興趣的可以看我同事的博客 Curator的ZK分布式鎖實(shí)現(xiàn)原理 。

總結(jié)

zookeeper 天生設(shè)計(jì)定位就是分布式協(xié)調(diào),強(qiáng)一致性,鎖很健壯。如果獲取不到鎖,只需要添加一個(gè)監(jiān)聽(tīng)器就可以了,不用一直輪詢(xún),性能消耗較小。缺點(diǎn): 在高請(qǐng)求高并發(fā)下,系統(tǒng)瘋狂的加鎖釋放鎖,最后 zk 承受不住這么大的壓力可能會(huì)存在宕機(jī)的風(fēng)險(xiǎn)。

在這里簡(jiǎn)單的提一下,zk 鎖性能比 redis 低的原因:zk 中的角色分為 leader,flower,每次寫(xiě)請(qǐng)求只能請(qǐng)求 leader,leader 會(huì)把寫(xiě)請(qǐng)求廣播到所有 flower,如果 flower 都成功才會(huì)提交給 leader,其實(shí)這里相當(dāng)于一個(gè) 2PC 的過(guò)程。在加鎖的時(shí)候是一個(gè)寫(xiě)請(qǐng)求,當(dāng)寫(xiě)請(qǐng)求很多時(shí),zk 會(huì)有很大的壓力,最后導(dǎo)致服務(wù)器響應(yīng)很慢。

redis 鎖實(shí)現(xiàn)簡(jiǎn)單,理解邏輯簡(jiǎn)單,性能好,可以支撐高并發(fā)的獲取、釋放鎖操作。缺點(diǎn): Redis 容易單點(diǎn)故障,集群部署,并不是強(qiáng)一致性的,鎖的不夠健壯; key 的過(guò)期時(shí)間設(shè)置多少不明確,只能根據(jù)實(shí)際情況調(diào)整;需要自己不斷去嘗試獲取鎖,比較消耗性能。

最后不管 redis 還是 zookeeper,它們都應(yīng)滿足分布式鎖的特性:

  • 具備可重入特性(已經(jīng)獲得鎖的線程在執(zhí)行的過(guò)程中不需要再次獲得鎖)
  • 異?;蛘叱瑫r(shí)自動(dòng)刪除,避免死鎖
  • 互斥(在分布式環(huán)境下同一時(shí)刻只能被單個(gè)線程獲?。?/li>
  • 分布式環(huán)境下高性能、高可用、容錯(cuò)機(jī)制

各有千秋,具體業(yè)務(wù)場(chǎng)景具體使用。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多

    欧美日韩一区二区三区色拉拉| 欧美日韩三区在线观看| 国产精品一区二区成人在线| 国产传媒高清视频在线| 日本加勒比系列在线播放| 久久精品福利在线观看| 99久久成人精品国产免费| 亚洲第一区二区三区女厕偷拍| 一区二区三区日本高清| 中字幕一区二区三区久久蜜桃| 亚洲中文字幕综合网在线| 好吊妞在线免费观看视频| 91亚洲人人在字幕国产| 夫妻性生活真人动作视频 | 亚洲国产成人精品福利| 国产麻豆精品福利在线| 日韩国产亚洲欧美激情| 国产91麻豆精品成人区| 冬爱琴音一区二区中文字幕| 日韩中文字幕免费在线视频| 粉嫩内射av一区二区| 日韩一区二区三区四区乱码视频| 好吊妞视频免费在线观看| 日本成人三级在线播放| 亚洲国产黄色精品在线观看| 国产一区二区在线免费| 男女午夜视频在线观看免费| 国产传媒中文字幕东京热| 国产精品丝袜一二三区| 青青操成人免费在线视频| 国产精品伦一区二区三区四季| 欧美日韩一区二区三区色拉拉| 极品少妇一区二区三区精品视频 | 在线视频三区日本精品| 日韩在线中文字幕不卡| 欧美日韩成人在线一区| 日本黄色高清视频久久| 亚洲天堂精品一区二区| 精品伊人久久大香线蕉综合| 国产午夜在线精品视频| 在线九月婷婷丁香伊人|