什么是分布式鎖?大家好,,我是jack xu,,今天跟大家聊一聊分布式鎖。首先說下什么是分布式鎖,,當(dāng)我們在進(jìn)行下訂單減庫存,,搶票,選課,,搶紅包這些業(yè)務(wù)場景時,,如果在此處沒有鎖的控制,會導(dǎo)致很嚴(yán)重的問題,。學(xué)過多線程的小伙們知道,,為了防止多個線程同時執(zhí)行同一段代碼,我們可以用 synchronized 關(guān)鍵字或 JUC 里面的 ReentrantLock 類來控制,,但是目前幾乎任何一個系統(tǒng)都是部署多臺機(jī)器的,,單機(jī)部署的應(yīng)用很少,synchronized 和 ReentrantLock 發(fā)揮不出任何作用,,此時就需要一把全局的鎖,,來代替 JAVA 中的 synchronized 和 ReentrantLock。 分布式鎖的實現(xiàn)方式流行的主要有三種,,分別是基于緩存 Redis 的實現(xiàn)方式,,基于 zk 臨時順序節(jié)點的實現(xiàn)以及基于數(shù)據(jù)庫行鎖的實現(xiàn)。我們先來說下用 Jedis 中的 setnx 命令來構(gòu)建這把鎖。 Jedis寫法使用 Redis 做分布式鎖的思路是,,在 redis 中設(shè)置一個值表示加了鎖,,然后釋放鎖的時候就把這個 key 刪除。思路是很簡單,,但是在使用過程中要避免一些坑,,我們先看下加鎖的代碼:
這段代碼很簡單,主要說下這里用的命令是 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL],,而沒有使用 SETNX+EXPIRE 的命令,,原因是 SETNX+EXPIRE 是兩條命令無法保證原子性,而 SET 是原子操作,。那這里為什么要設(shè)置超時時間呢,?原因是當(dāng)一個客戶端獲得了鎖在執(zhí)行任務(wù)的過程中掛掉了,來不及顯式地釋放鎖,,這塊資源將會永遠(yuǎn)被鎖住,,這將會導(dǎo)致死鎖,所以必須設(shè)置一個超時時間,。 釋放鎖的代碼如下:
這里也有兩點注意的地方,,第一是解鈴還須系鈴人,怎么理解呢,,就是 A 加的鎖 B 不能去 del 掉吧,,不然豈不是全亂套了,誰加的鎖就誰去解,,我們一般把 value 設(shè)為當(dāng)前線程的 Id,,Thread.currentThread().getId(),然后在刪的時候判斷下是不是當(dāng)前線程,。第二點是驗證和釋放鎖是兩個獨立操作,,不是原子性,這個怎么解決呢,?使用 Lua 腳本,,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能給我們保證原子性,。 Redisson寫法Redisson 是 Java 的 Redis 客戶端之一,,提供了一些 API 方便操作 Redis。但是 Redisson 這個客戶端可有點厲害,,我們先打開官網(wǎng)看下 github.com/redisson/re… image
這個目錄里面有很多的功能,,Redisson 跟 Jedis 定位不同,它不是一個單純的 Redis 客戶端,,而是基于 Redis 實現(xiàn)的分布式的服務(wù),,我們可以看到還有 JUC 包下面的類名,,Redisson 幫我們搞了分布式的版本,比如 AtomicLong,,直接用 RedissonAtomicLong 就行了,。鎖只是它的冰山一角,并且它對主從,,哨兵,,集群等模式都支持,,當(dāng)然了,,單節(jié)點模式肯定是支持的。 在 Redisson 里面提供了更加簡單的分布式鎖的實現(xiàn),,我們來看下它的用法,,相當(dāng)?shù)暮唵危瑑尚写a搞定,,比 Jedis 要簡單的多,,而且在 Jedis 里需要考慮的問題,它都已經(jīng)幫我們封裝好了,。 image
我們來看下,,這里獲取鎖有很多種的方式,有公平鎖有讀寫鎖,,我們使用的是 redissonClient.getLock,, 這是一個可重入鎖。 image
現(xiàn)在我把程序啟動一下 image
打開 Redis Desktop Manager 工具,,看下到底它存的是什么,。原來在加鎖的時候,寫入了一個 HASH 類型的值,,key 是鎖名稱 jackxu,,field 是線程的名稱,而 value 是 1(即表示鎖的重入次數(shù)),。 image
小伙伴可能覺得我在一派胡言,,沒關(guān)系,我們點進(jìn)去看下它的源碼是具體實現(xiàn)的,。 image
點進(jìn) tryLock() 方法的 tryAcquire() 方法,,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),終于見到廬山真面目了,,原來它最終也是通過 Lua 腳本來實現(xiàn)的,。 image
現(xiàn)在我把這段Lua腳本拿出來分析一下,很簡單,。
unlock() 中的 unlockInnerAsync() 釋放鎖,,同樣也是通過 Lua 腳本實現(xiàn),。
看完它的使用后,,我們發(fā)現(xiàn)真的使用起來像 JDK 中的 ReentrantLock 一樣絲滑,。 image
RedLockRedLock 的中文是直譯過來的,就叫紅鎖,。紅鎖并非是一個工具,,而是 Redis 官方提出的一種分布式鎖的算法。我們知道如果采用單機(jī)部署模式,,會存在單點問題,,只要 redis 故障了,加鎖就不行了,。如果采用 master-slave 模式,,加鎖的時候只對一個節(jié)點加鎖,即便通過 sentinel 做了高可用,,但是如果 master 節(jié)點故障了,,發(fā)生主從切換,此時就會有可能出現(xiàn)鎖丟失的問題,?;谝陨系目紤],其實 redis 的作者 Antirez 也考慮到這個問題,,他提出了一個 RedLock 的算法,。 我在這里畫了一個圖,圖中這五個實例都是獨自部署的,,沒有主從關(guān)系,,它們就是5個 master 節(jié)點。 image
通過以下步驟獲取一把鎖:
但是這樣的這種算法還是頗具爭議的,,可能還會存在不少的問題,無法保證加鎖的過程一定正確,。Martin Kleppmann 針對這個算法提出了質(zhì)疑,,接著 antirez 又回復(fù)了 Martin Kleppmann 的質(zhì)疑。一個是很有資歷的分布式架構(gòu)師,,一個是 Redis 之父,,這個就是著名的關(guān)于紅鎖的神仙打架事件。 最后 Redisson 官網(wǎng)上也給出了如何使用紅鎖 redlock,,幾行代碼搞定,依然很絲滑,,感興趣的小伙伴可以看下,。 image
Zookeeper寫法在介紹 zookeeper 實現(xiàn)分布式鎖的機(jī)制之前,先粗略介紹一下 zk 是什么東西: zk 是一種提供配置管理,、分布式協(xié)同以及命名的中心化服務(wù),。它的模型是這樣的:包含一系列的節(jié)點,叫做znode,,就好像文件系統(tǒng)一樣每個 znode 表示一個目錄,,然后 znode 有一些特性,我們可以把它們分為四類:
zookeeper分布式鎖恰恰應(yīng)用了臨時順序節(jié)點,,下面我們就用圖解的方式來看下是怎么實現(xiàn)的,。 獲取鎖首先,在 Zookeeper 當(dāng)中創(chuàng)建一個持久節(jié)點 ParentLock,。當(dāng)?shù)谝粋€客戶端想要獲得鎖時,,需要在ParentLock這個節(jié)點下面創(chuàng)建一個臨時順序節(jié)點 Lock1。 image
之后,,Client1 查找 ParentLock 下面所有的臨時順序節(jié)點并排序,,判斷自己所創(chuàng)建的節(jié)點 Lock1 是不是順序最靠前的一個。如果是第一個節(jié)點,,則成功獲得鎖,。 image
這時候,,如果再有一個客戶端 Client2 前來獲取鎖,則在 ParentLock 下再創(chuàng)建一個臨時順序節(jié)點Lock2,。 image
Client2 查找 ParentLock 下面所有的臨時順序節(jié)點并排序,,判斷自己所創(chuàng)建的節(jié)點 Lock2 是不是順序最靠前的一個,結(jié)果發(fā)現(xiàn)節(jié)點 Lock2 并不是最小的,。 于是,,Client2 向排序僅比它靠前的節(jié)點 Lock1 注冊 Watcher,用于監(jiān)聽 Lock1 節(jié)點是否存在,。這意味著 Client2 搶鎖失敗,,進(jìn)入了等待狀態(tài)。 image
這時候,,如果又有一個客戶端 Client3 前來獲取鎖,,則在ParentLock下載再創(chuàng)建一個臨時順序節(jié)點Lock3。 image
Client3 查找 ParentLock 下面所有的臨時順序節(jié)點并排序,,判斷自己所創(chuàng)建的節(jié)點 Lock3 是不是順序最靠前的一個,,結(jié)果同樣發(fā)現(xiàn)節(jié)點 Lock3 并不是最小的。 于是,,Client3 向排序僅比它靠前的節(jié)點 Lock2 注冊 Watcher,,用于監(jiān)聽 Lock2 節(jié)點是否存在。這意味著 Client3 同樣搶鎖失敗,,進(jìn)入了等待狀態(tài),。 image
這樣一來,Client1 得到了鎖,,Client2 監(jiān)聽了 Lock1,,Client3 監(jiān)聽了 Lock2。這恰恰形成了一個等待隊列,,很像是 Java 當(dāng)中 ReentrantLock 所依賴的 AQS(AbstractQueuedSynchronizer),。 釋放鎖釋放鎖分為兩種情況: 1.任務(wù)完成,客戶端顯示釋放 當(dāng)任務(wù)完成時,,Client1 會顯示調(diào)用刪除節(jié)點 Lock1 的指令,。 image
2.任務(wù)執(zhí)行過程中,客戶端崩潰 獲得鎖的 Client1 在任務(wù)執(zhí)行過程中,,如果 Duang 的一聲崩潰,,則會斷開與 Zookeeper 服務(wù)端的鏈接。根據(jù)臨時節(jié)點的特性,,相關(guān)聯(lián)的節(jié)點 Lock1 會隨之自動刪除,。 image
由于 Client2 一直監(jiān)聽著 Lock1 的存在狀態(tài),當(dāng) Lock1 節(jié)點被刪除,,Client2 會立刻收到通知,。這時候 Client2 會再次查詢 ParentLock 下面的所有節(jié)點,,確認(rèn)自己創(chuàng)建的節(jié)點 Lock2 是不是目前最小的節(jié)點。如果是最小,,則 Client2 順理成章獲得了鎖,。 image
同理,如果 Client2 也因為任務(wù)完成或者節(jié)點崩潰而刪除了節(jié)點 Lock2,,那么 Client3 就會接到通知,。 image
最終,Client3 成功得到了鎖,。 image
Curator在 Apache 的開源框架 Apache Curator 中,,包含了對 Zookeeper 分布式鎖的實現(xiàn)。 github.com/apache/cura… 它的使用方式也很簡單,,如下所示: image
我們看了下依然絲滑,,源碼我就不分析了,感興趣的可以看我同事的博客 Curator的ZK分布式鎖實現(xiàn)原理 ,。 總結(jié)zookeeper 天生設(shè)計定位就是分布式協(xié)調(diào),,強(qiáng)一致性,鎖很健壯,。如果獲取不到鎖,,只需要添加一個監(jiān)聽器就可以了,不用一直輪詢,,性能消耗較小。缺點: 在高請求高并發(fā)下,,系統(tǒng)瘋狂的加鎖釋放鎖,,最后 zk 承受不住這么大的壓力可能會存在宕機(jī)的風(fēng)險。
redis 鎖實現(xiàn)簡單,理解邏輯簡單,,性能好,,可以支撐高并發(fā)的獲取,、釋放鎖操作。缺點: Redis 容易單點故障,,集群部署,,并不是強(qiáng)一致性的,鎖的不夠健壯,; key 的過期時間設(shè)置多少不明確,,只能根據(jù)實際情況調(diào)整;需要自己不斷去嘗試獲取鎖,,比較消耗性能,。 最后不管 redis 還是 zookeeper,它們都應(yīng)滿足分布式鎖的特性:
各有千秋,具體業(yè)務(wù)場景具體使用,。 |
|