Java分布式鎖看這篇就夠了
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??seesun2012
來源 |? urlify.cn/uY36R3
66套java從入門到精通實(shí)戰(zhàn)課程分享?
什么是鎖?
在單進(jìn)程的系統(tǒng)中,當(dāng)存在多個(gè)線程可以同時(shí)改變某個(gè)變量(可變共享變量)時(shí),就需要對(duì)變量或代碼塊做同步,使其在修改這種變量時(shí)能夠線性執(zhí)行消除并發(fā)修改變量。
而同步的本質(zhì)是通過鎖來實(shí)現(xiàn)的。為了實(shí)現(xiàn)多個(gè)線程在一個(gè)時(shí)刻同一個(gè)代碼塊只能有一個(gè)線程可執(zhí)行,那么需要在某個(gè)地方做個(gè)標(biāo)記,這個(gè)標(biāo)記必須每個(gè)線程都能看到,當(dāng)標(biāo)記不存在時(shí)可以設(shè)置該標(biāo)記,其余后續(xù)線程發(fā)現(xiàn)已經(jīng)有標(biāo)記了則等待擁有標(biāo)記的線程結(jié)束同步代碼塊取消標(biāo)記后再去嘗試設(shè)置標(biāo)記。這個(gè)標(biāo)記可以理解為鎖。
不同地方實(shí)現(xiàn)鎖的方式也不一樣,只要能滿足所有線程都能看得到標(biāo)記即可。如 Java 中 synchronize 是在對(duì)象頭設(shè)置標(biāo)記,Lock 接口的實(shí)現(xiàn)類基本上都只是某一個(gè) volitile 修飾的 int 型變量其保證每個(gè)線程都能擁有對(duì)該 int 的可見性和原子修改,linux 內(nèi)核中也是利用互斥量或信號(hào)量等內(nèi)存數(shù)據(jù)做標(biāo)記。
除了利用內(nèi)存數(shù)據(jù)做鎖其實(shí)任何互斥的都能做鎖(只考慮互斥情況),如流水表中流水號(hào)與時(shí)間結(jié)合做冪等校驗(yàn)可以看作是一個(gè)不會(huì)釋放的鎖,或者使用某個(gè)文件是否存在作為鎖等。只需要滿足在對(duì)標(biāo)記進(jìn)行修改能保證原子性和內(nèi)存可見性即可。
什么是分布式?
分布式的 CAP 理論告訴我們:
任何一個(gè)分布式系統(tǒng)都無法同時(shí)滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯(cuò)性(Partition tolerance),最多只能同時(shí)滿足兩項(xiàng)。
目前很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場(chǎng)景中的數(shù)據(jù)一致性問題一直是一個(gè)比較重要的話題。基于 CAP理論,很多系統(tǒng)在設(shè)計(jì)之初就要對(duì)這三者做出取舍。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場(chǎng)景中,都需要犧牲強(qiáng)一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證最終一致性。
分布式場(chǎng)景
此處主要指集群模式下,多個(gè)相同服務(wù)同時(shí)開啟.
在許多的場(chǎng)景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。很多時(shí)候我們需要保證一個(gè)方法在同一時(shí)間內(nèi)只能被同一個(gè)線程執(zhí)行。在單機(jī)環(huán)境中,通過 Java 提供的并發(fā) API 我們可以解決,但是在分布式環(huán)境下,就沒有那么簡單啦。
分布式與單機(jī)情況下最大的不同在于其不是多線程而是
多進(jìn)程。多線程由于可以共享堆內(nèi)存,因此可以簡單的采取內(nèi)存作為標(biāo)記存儲(chǔ)位置。而進(jìn)程之間甚至可能都不在同一臺(tái)物理機(jī)上,因此需要將標(biāo)記存儲(chǔ)在一個(gè)所有進(jìn)程都能看到的地方。
什么是分布式鎖?
當(dāng)在分布式模型下,數(shù)據(jù)只有一份(或有限制),此時(shí)需要利用鎖的技術(shù)控制某一時(shí)刻修改數(shù)據(jù)的進(jìn)程數(shù)。
與單機(jī)模式下的鎖不僅需要保證進(jìn)程可見,還需要考慮進(jìn)程與鎖之間的網(wǎng)絡(luò)問題。(我覺得分布式情況下之所以問題變得復(fù)雜,主要就是需要考慮到網(wǎng)絡(luò)的延時(shí)和不可靠。。。一個(gè)大坑)
分布式鎖還是可以將標(biāo)記存在內(nèi)存,只是該內(nèi)存不是某個(gè)進(jìn)程分配的內(nèi)存而是公共內(nèi)存如 Redis、Memcache。至于利用數(shù)據(jù)庫、文件等做鎖與單機(jī)的實(shí)現(xiàn)是一樣的,只要保證標(biāo)記能互斥就行。
我們需要怎樣的分布式鎖?
可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器-上的一個(gè)線程執(zhí)行。
這把鎖要是一把可重入鎖(避免死鎖)
這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
這把鎖最好是一把公平鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
有高可用的獲取鎖和釋放鎖功能
獲取鎖和釋放鎖的性能要好
基于數(shù)據(jù)庫做分布式鎖
基于樂觀鎖
基于表主鍵唯一做分布式鎖
思路:利用主鍵唯一的特性,如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,刪除這條數(shù)據(jù)庫記錄即可。
上面這種簡單的實(shí)現(xiàn)有以下幾個(gè)問題:
這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
這把鎖沒有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的 insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
這把鎖是非重入的,同一個(gè)線程在沒有釋放鎖之前無法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
這把鎖是非公平鎖,所有等待鎖的線程憑運(yùn)氣去爭奪鎖。
在 MySQL 數(shù)據(jù)庫中采用主鍵沖突防重,在大并發(fā)情況下有可能會(huì)造成鎖表現(xiàn)象。
當(dāng)然,我們也可以有其他方式解決上面的問題。
數(shù)據(jù)庫是單點(diǎn)?搞兩個(gè)數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步,一旦掛掉快速切換到備庫上。
沒有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫中的超時(shí)數(shù)據(jù)清理一遍。
非阻塞的?搞一個(gè) while 循環(huán),直到 insert 成功再返回成功。
非重入的?在數(shù)據(jù)庫表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給他就可以了。
非公平的?再建一張中間表,將等待鎖的線程全記錄下來,并根據(jù)創(chuàng)建時(shí)間排序,只有最先創(chuàng)建的允許獲取鎖。
比較好的辦法是在程序中生產(chǎn)主鍵進(jìn)行防重。
基于表字段版本號(hào)做分布式鎖
這個(gè)策略源于 mysql 的 mvcc 機(jī)制,使用這個(gè)策略其實(shí)本身沒有什么問題,唯一的問題就是對(duì)數(shù)據(jù)表侵入較大,我們要為每個(gè)表設(shè)計(jì)一個(gè)版本號(hào)字段,然后寫一條判斷 sql 每次進(jìn)行判斷,增加了數(shù)據(jù)庫操作的次數(shù),在高并發(fā)的要求下,對(duì)數(shù)據(jù)庫連接的開銷也是無法忍受的。
基于悲觀鎖
基于數(shù)據(jù)庫排他鎖做分布式鎖
在查詢語句后面增加for update,數(shù)據(jù)庫會(huì)在查詢過程中給數(shù)據(jù)庫表增加排他鎖 (注意:InnoDB 引擎在加鎖的時(shí)候,只有通過索引進(jìn)行檢索的時(shí)候才會(huì)使用行級(jí)鎖,否則會(huì)使用表級(jí)鎖。這里我們希望使用行級(jí)鎖,就要給要執(zhí)行的方法字段名添加索引,值得注意的是,這個(gè)索引一定要?jiǎng)?chuàng)建成唯一索引,否則會(huì)出現(xiàn)多個(gè)重載方法之間無法同時(shí)被訪問的問題。重載方法的話建議把參數(shù)類型也加上。)。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
我們可以認(rèn)為獲得排他鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖??
for update語句會(huì)在執(zhí)行成功后立即返回,在執(zhí)行失敗時(shí)一直處于阻塞狀態(tài),直到成功。鎖定之后服務(wù)宕機(jī),無法釋放?使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫會(huì)自己把鎖釋放掉。
但是還是無法直接解決數(shù)據(jù)庫單點(diǎn)和可重入問題。
這里還可能存在另外一個(gè)問題,雖然我們對(duì)方法字段名使用了唯一索引,并且顯示使用 for update 來使用行級(jí)鎖。但是,MySQL 會(huì)對(duì)查詢進(jìn)行優(yōu)化,即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計(jì)劃的代價(jià)來決定的,如果 MySQL 認(rèn)為全表掃效率更高,比如對(duì)一些很小的表,它就不會(huì)使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。如果發(fā)生這種情況就悲劇了。。。
還有一個(gè)問題,就是我們要使用排他鎖來進(jìn)行分布式鎖的 lock,那么一個(gè)排他鎖長時(shí)間不提交,就會(huì)占用數(shù)據(jù)庫連接。一旦類似的連接變得多了,就可能把數(shù)據(jù)庫連接池?fù)伪?/span>
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡單,易于理解
缺點(diǎn):會(huì)有各種各樣的問題(操作數(shù)據(jù)庫需要一定的開銷,使用數(shù)據(jù)庫的行級(jí)鎖并不一定靠譜,性能不靠譜)
基于 Redis 做分布式鎖
基于 REDIS 的 SETNX()、EXPIRE() 方法做分布式鎖
setnx()
setnx 的含義就是 SET if Not Exists,其主要有兩個(gè)參數(shù) setnx(key, value)。該方法是原子的,如果 key 不存在,則設(shè)置當(dāng)前 key 成功,返回 1;如果當(dāng)前 key 已經(jīng)存在,則設(shè)置當(dāng)前 key 失敗,返回 0。
expire()
expire 設(shè)置過期時(shí)間,要注意的是 setnx 命令不能設(shè)置 key 的超時(shí)時(shí)間,只能通過 expire() 來對(duì) key 設(shè)置。
使用步驟
1、setnx(lockkey, 1) 如果返回 0,則說明占位失敗;如果返回 1,則說明占位成功
2、expire() 命令對(duì) lockkey 設(shè)置超時(shí)時(shí)間,為的是避免死鎖問題。
3、執(zhí)行完業(yè)務(wù)代碼后,可以通過 delete 命令刪除 key。
這個(gè)方案其實(shí)是可以解決日常工作中的需求的,但從技術(shù)方案的探討上來說,可能還有一些可以完善的地方。比如,如果在第一步 setnx 執(zhí)行成功后,在 expire() 命令執(zhí)行成功前,發(fā)生了宕機(jī)的現(xiàn)象,那么就依然會(huì)出現(xiàn)死鎖的問題,所以如果要對(duì)其進(jìn)行完善的話,可以使用 redis 的 setnx()、get() 和 getset() 方法來實(shí)現(xiàn)分布式鎖。
基于 REDIS 的 SETNX()、GET()、GETSET()方法做分布式鎖
這個(gè)方案的背景主要是在 setnx() 和 expire() 的方案上針對(duì)可能存在的死鎖問題,做了一些優(yōu)化。
getset()
這個(gè)命令主要有兩個(gè)參數(shù) getset(key,newValue)。該方法是原子的,對(duì) key 設(shè)置 newValue 這個(gè)值,并且返回 key 原來的舊值。假設(shè) key 原來是不存在的,那么多次執(zhí)行這個(gè)命令,會(huì)出現(xiàn)下邊的效果:
getset(key, “value1”) 返回 null 此時(shí) key 的值會(huì)被設(shè)置為 value1
getset(key, “value2”) 返回 value1 此時(shí) key 的值會(huì)被設(shè)置為 value2
依次類推!
使用步驟
setnx(lockkey, 當(dāng)前時(shí)間+過期超時(shí)時(shí)間),如果返回 1,則獲取鎖成功;如果返回 0 則沒有獲取到鎖,轉(zhuǎn)向 2。
get(lockkey) 獲取值 oldExpireTime ,并將這個(gè) value 值與當(dāng)前的系統(tǒng)時(shí)間進(jìn)行比較,如果小于當(dāng)前系統(tǒng)時(shí)間,則認(rèn)為這個(gè)鎖已經(jīng)超時(shí),可以允許別的請(qǐng)求重新獲取,轉(zhuǎn)向 3。
計(jì)算 newExpireTime = 當(dāng)前時(shí)間+過期超時(shí)時(shí)間,然后 getset(lockkey, newExpireTime) 會(huì)返回當(dāng)前 lockkey 的值currentExpireTime。
判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,說明當(dāng)前 getset 設(shè)置成功,獲取到了鎖。如果不相等,說明這個(gè)鎖又被別的請(qǐng)求獲取走了,那么當(dāng)前請(qǐng)求可以直接返回失敗,或者繼續(xù)重試。
在獲取到鎖之后,當(dāng)前線程可以開始自己的業(yè)務(wù)處理,當(dāng)處理完畢后,比較自己的處理時(shí)間和對(duì)于鎖設(shè)置的超時(shí)時(shí)間,如果小于鎖設(shè)置的超時(shí)時(shí)間,則直接執(zhí)行 delete 釋放鎖;如果大于鎖設(shè)置的超時(shí)時(shí)間,則不需要再鎖進(jìn)行處理。
import?cn.com.tpig.cache.redis.RedisService;
import?cn.com.tpig.utils.SpringUtils;
//redis分布式鎖
public?final?class?RedisLockUtil?{
????private?static?final?int?defaultExpire = 60;
????private?RedisLockUtil()?{
????????//
????}
????/**
?????* 加鎖
?????* @param?key redis key
?????* @param?expire 過期時(shí)間,單位秒
?????* @return?true:加鎖成功,false,加鎖失敗
?????*/
????public?static?boolean?lock(String key, int?expire)?{
????????RedisService redisService = SpringUtils.getBean(RedisService.class);
????????long?status = redisService.setnx(key, "1");
????????if(status == 1) {
????????????redisService.expire(key, expire);
????????????return?true;
????????}
????????return?false;
????}
????public?static?boolean?lock(String key)?{
????????return?lock2(key, defaultExpire);
????}
????/**
?????* 加鎖
?????* @param?key redis key
?????* @param?expire 過期時(shí)間,單位秒
?????* @return?true:加鎖成功,false,加鎖失敗
?????*/
????public?static?boolean?lock2(String key, int?expire)?{
????????RedisService redisService = SpringUtils.getBean(RedisService.class);
????????long?value = System.currentTimeMillis() + expire;
????????long?status = redisService.setnx(key, String.valueOf(value));
????????if(status == 1) {
????????????return?true;
????????}
????????long?oldExpireTime = Long.parseLong(redisService.get(key, "0"));
????????if(oldExpireTime < System.currentTimeMillis()) {
????????????//超時(shí)
????????????long?newExpireTime = System.currentTimeMillis() + expire;
????????????long?currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));
????????????if(currentExpireTime == oldExpireTime) {
????????????????return?true;
????????????}
????????}
????????return?false;
????}
????public?static?void?unLock1(String key)?{
????????RedisService redisService = SpringUtils.getBean(RedisService.class);
????????redisService.del(key);
????}
????public?static?void?unLock2(String key)?{
????????RedisService redisService = SpringUtils.getBean(RedisService.class);
????????long?oldExpireTime = Long.parseLong(redisService.get(key, "0"));
????????if(oldExpireTime > System.currentTimeMillis()) {
????????????redisService.del(key);
????????}
???}
}public?void?drawRedPacket(long?userId) {
????String key = "draw.redpacket.userid:"?+ userId;
????boolean lock?= RedisLockUtil.lock2(key, 60);
????if(lock) {
????????try?{
????????????//領(lǐng)取操作
????????} finally?{
????????????//釋放鎖
????????????RedisLockUtil.unLock(key);
????????}
????} else?{
????????new?RuntimeException("重復(fù)領(lǐng)取獎(jiǎng)勵(lì)");
????}
}基于 REDLOCK 做分布式鎖
Redlock 是 Redis 的作者 antirez 給出的集群模式的 Redis 分布式鎖,它基于 N 個(gè)完全獨(dú)立的 Redis 節(jié)點(diǎn)(通常情況下 N 可以設(shè)置成 5)。
算法的步驟如下:
1、客戶端獲取當(dāng)前時(shí)間,以毫秒為單位。
2、客戶端嘗試獲取 N 個(gè)節(jié)點(diǎn)的鎖,(每個(gè)節(jié)點(diǎn)獲取鎖的方式和前面說的緩存鎖一樣),N 個(gè)節(jié)點(diǎn)以相同的 key 和 value 獲取鎖。客戶端需要設(shè)置接口訪問超時(shí),接口超時(shí)時(shí)間需要遠(yuǎn)遠(yuǎn)小于鎖超時(shí)時(shí)間,比如鎖自動(dòng)釋放的時(shí)間是 10s,那么接口超時(shí)大概設(shè)置 5-50ms。這樣可以在有 redis 節(jié)點(diǎn)宕機(jī)后,訪問該節(jié)點(diǎn)時(shí)能盡快超時(shí),而減小鎖的正常使用。
3、客戶端計(jì)算在獲得鎖的時(shí)候花費(fèi)了多少時(shí)間,方法是用當(dāng)前時(shí)間減去在步驟一獲取的時(shí)間,只有客戶端獲得了超過 3 個(gè)節(jié)點(diǎn)的鎖,而且獲取鎖的時(shí)間小于鎖的超時(shí)時(shí)間,客戶端才獲得了分布式鎖。
4、客戶端獲取的鎖的時(shí)間為設(shè)置的鎖超時(shí)時(shí)間減去步驟三計(jì)算出的獲取鎖花費(fèi)時(shí)間。
5、如果客戶端獲取鎖失敗了,客戶端會(huì)依次刪除所有的鎖。
使用 Redlock 算法,可以保證在掛掉最多 2 個(gè)節(jié)點(diǎn)的時(shí)候,分布式鎖服務(wù)仍然能工作,這相比之前的數(shù)據(jù)庫鎖和緩存鎖大大提高了可用性,由于 redis 的高效性能,分布式緩存鎖性能并不比數(shù)據(jù)庫鎖差。
但是,有一位分布式的專家寫了一篇文章《How to do distributed locking》,質(zhì)疑 Redlock 的正確性。
https://mp.weixin.qq.com/s/1bPLk_VZhZ0QYNZS8LkviA
https://blog.csdn.net/jek123456/article/details/72954106
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):?性能高
缺點(diǎn):
失效時(shí)間設(shè)置多長時(shí)間為好?如何設(shè)置的失效時(shí)間太短,方法沒等執(zhí)行完,鎖就自動(dòng)釋放了,那么就會(huì)產(chǎn)生并發(fā)問題。如果設(shè)置的時(shí)間太長,其他獲取鎖的線程就可能要平白的多等一段時(shí)間。
基于 REDISSON 做分布式鎖
redisson 是 redis 官方的分布式鎖組件。GitHub 地址:https://github.com/redisson/redisson
上面的這個(gè)問題 ——> 失效時(shí)間設(shè)置多長時(shí)間為好?這個(gè)問題在 redisson 的做法是:每獲得一個(gè)鎖時(shí),只設(shè)置一個(gè)很短的超時(shí)時(shí)間,同時(shí)起一個(gè)線程在每次快要到超時(shí)時(shí)間時(shí)去刷新鎖的超時(shí)時(shí)間。在釋放鎖的同時(shí)結(jié)束這個(gè)線程。
基于 ZooKeeper 做分布式鎖
ZOOKEEPER 鎖相關(guān)基礎(chǔ)知識(shí)
zk 一般由多個(gè)節(jié)點(diǎn)構(gòu)成(單數(shù)),采用 zab 一致性協(xié)議。因此可以將 zk 看成一個(gè)單點(diǎn)結(jié)構(gòu),對(duì)其修改數(shù)據(jù)其內(nèi)部自動(dòng)將所有節(jié)點(diǎn)數(shù)據(jù)進(jìn)行修改而后才提供查詢服務(wù)。
zk 的數(shù)據(jù)以目錄樹的形式,每個(gè)目錄稱為 znode, znode 中可存儲(chǔ)數(shù)據(jù)(一般不超過 1M),還可以在其中增加子節(jié)點(diǎn)。
子節(jié)點(diǎn)有三種類型。序列化節(jié)點(diǎn),每在該節(jié)點(diǎn)下增加一個(gè)節(jié)點(diǎn)自動(dòng)給該節(jié)點(diǎn)的名稱上自增。臨時(shí)節(jié)點(diǎn),一旦創(chuàng)建這個(gè) znode 的客戶端與服務(wù)器失去聯(lián)系,這個(gè) znode 也將自動(dòng)刪除。最后就是普通節(jié)點(diǎn)。
Watch 機(jī)制,client 可以監(jiān)控每個(gè)節(jié)點(diǎn)的變化,當(dāng)產(chǎn)生變化會(huì)給 client 產(chǎn)生一個(gè)事件。
ZK 基本鎖
原理:利用臨時(shí)節(jié)點(diǎn)與 watch 機(jī)制。每個(gè)鎖占用一個(gè)普通節(jié)點(diǎn) /lock,當(dāng)需要獲取鎖時(shí)在 /lock 目錄下創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),創(chuàng)建成功則表示獲取鎖成功,失敗則 watch/lock 節(jié)點(diǎn),有刪除操作后再去爭鎖。臨時(shí)節(jié)點(diǎn)好處在于當(dāng)進(jìn)程掛掉后能自動(dòng)上鎖的節(jié)點(diǎn)自動(dòng)刪除即取消鎖。
缺點(diǎn):所有取鎖失敗的進(jìn)程都監(jiān)聽父節(jié)點(diǎn),很容易發(fā)生羊群效應(yīng),即當(dāng)釋放鎖后所有等待進(jìn)程一起來創(chuàng)建節(jié)點(diǎn),并發(fā)量很大。
ZK 鎖優(yōu)化
原理:上鎖改為創(chuàng)建臨時(shí)有序節(jié)點(diǎn),每個(gè)上鎖的節(jié)點(diǎn)均能創(chuàng)建節(jié)點(diǎn)成功,只是其序號(hào)不同。只有序號(hào)最小的可以擁有鎖,如果這個(gè)節(jié)點(diǎn)序號(hào)不是最小的則 watch 序號(hào)比本身小的前一個(gè)節(jié)點(diǎn) (公平鎖)。
步驟:
1.在 /lock 節(jié)點(diǎn)下創(chuàng)建一個(gè)有序臨時(shí)節(jié)點(diǎn) (EPHEMERAL_SEQUENTIAL)。
2.判斷創(chuàng)建的節(jié)點(diǎn)序號(hào)是否最小,如果是最小則獲取鎖成功。不是則取鎖失敗,然后 watch 序號(hào)比本身小的前一個(gè)節(jié)點(diǎn)。
3.當(dāng)取鎖失敗,設(shè)置 watch 后則等待 watch 事件到來后,再次判斷是否序號(hào)最小。
4.取鎖成功則執(zhí)行代碼,最后釋放鎖(刪除該節(jié)點(diǎn))。
import?java.io.IOException;
import?java.util.ArrayList;
import?java.util.Collections;
import?java.util.List;
import?java.util.concurrent.CountDownLatch;
import?java.util.concurrent.TimeUnit;
import?java.util.concurrent.locks.Condition;
import?java.util.concurrent.locks.Lock;
import?org.apache.zookeeper.CreateMode;
import?org.apache.zookeeper.KeeperException;
import?org.apache.zookeeper.WatchedEvent;
import?org.apache.zookeeper.Watcher;
import?org.apache.zookeeper.ZooDefs;
import?org.apache.zookeeper.ZooKeeper;
import?org.apache.zookeeper.data.Stat;
public?class?DistributedLock?implements?Lock, Watcher{
????private?ZooKeeper zk;
????private?String root = "/locks";//根
????private?String lockName;//競爭資源的標(biāo)志
????private?String waitNode;//等待前一個(gè)鎖
????private?String myZnode;//當(dāng)前鎖
????private?CountDownLatch latch;//計(jì)數(shù)器
????private?int?sessionTimeout = 30000;
????private?List exception = new?ArrayList();
????/**
?????* 創(chuàng)建分布式鎖,使用前請(qǐng)確認(rèn)config配置的zookeeper服務(wù)可用
?????* @param?config 127.0.0.1:2181
?????* @param?lockName 競爭資源標(biāo)志,lockName中不能包含單詞lock
?????*/
????public?DistributedLock(String config, String lockName){
????????this.lockName = lockName;
????????// 創(chuàng)建一個(gè)與服務(wù)器的連接
????????try?{
????????????zk = new?ZooKeeper(config, sessionTimeout, this);
????????????Stat stat = zk.exists(root, false);
????????????if(stat == null){
????????????????// 創(chuàng)建根節(jié)點(diǎn)
????????????????zk.create(root, new?byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
????????????}
????????} catch?(IOException e) {
????????????exception.add(e);
????????} catch?(KeeperException e) {
????????????exception.add(e);
????????} catch?(InterruptedException e) {
????????????exception.add(e);
????????}
????}
????/**
?????* zookeeper節(jié)點(diǎn)的監(jiān)視器
?????*/
????public?void?process(WatchedEvent event)?{
????????if(this.latch != null) {
????????????this.latch.countDown();
????????}
????}
????public?void?lock()?{
????????if(exception.size() > 0){
????????????throw?new?LockException(exception.get(0));
????????}
????????try?{
????????????if(this.tryLock()){
????????????????System.out.println("Thread "?+ Thread.currentThread().getId() + " "?+myZnode + " get lock true");
????????????????return;
????????????}
????????????else{
????????????????waitForLock(waitNode, sessionTimeout);//等待鎖
????????????}
????????} catch?(KeeperException e) {
????????????throw?new?LockException(e);
????????} catch?(InterruptedException e) {
????????????throw?new?LockException(e);
????????}
????}
????public?boolean?tryLock()?{
????????try?{
????????????String splitStr = "_lock_";
????????????if(lockName.contains(splitStr))
????????????????throw?new?LockException("lockName can not contains \\u000B");
????????????//創(chuàng)建臨時(shí)子節(jié)點(diǎn)
????????????myZnode = zk.create(root + "/"?+ lockName + splitStr, new?byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
????????????System.out.println(myZnode + " is created ");
????????????//取出所有子節(jié)點(diǎn)
????????????List subNodes = zk.getChildren(root, false);
????????????//取出所有l(wèi)ockName的鎖
????????????List lockObjNodes = new?ArrayList();
????????????for?(String node : subNodes) {
????????????????String _node = node.split(splitStr)[0];
????????????????if(_node.equals(lockName)){
????????????????????lockObjNodes.add(node);
????????????????}
????????????}
????????????Collections.sort(lockObjNodes);
????????????System.out.println(myZnode + "=="?+ lockObjNodes.get(0));
????????????if(myZnode.equals(root+"/"+lockObjNodes.get(0))){
????????????????//如果是最小的節(jié)點(diǎn),則表示取得鎖
????????????????return?true;
????????????}
????????????//如果不是最小的節(jié)點(diǎn),找到比自己小1的節(jié)點(diǎn)
????????????String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
????????????waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
????????} catch?(KeeperException e) {
????????????throw?new?LockException(e);
????????} catch?(InterruptedException e) {
????????????throw?new?LockException(e);
????????}
????????return?false;
????}
????public?boolean?tryLock(long?time, TimeUnit unit)?{
????????try?{
????????????if(this.tryLock()){
????????????????return?true;
????????????}
????????????return?waitForLock(waitNode,time);
????????} catch?(Exception e) {
????????????e.printStackTrace();
????????}
????????return?false;
????}
????private?boolean?waitForLock(String lower, long?waitTime)?throws?InterruptedException, KeeperException {
????????Stat stat = zk.exists(root + "/"?+ lower,true);
????????//判斷比自己小一個(gè)數(shù)的節(jié)點(diǎn)是否存在,如果不存在則無需等待鎖,同時(shí)注冊(cè)監(jiān)聽
????????if(stat != null){
????????????System.out.println("Thread "?+ Thread.currentThread().getId() + " waiting for "?+ root + "/"?+ lower);
????????????this.latch = new?CountDownLatch(1);
????????????this.latch.await(waitTime, TimeUnit.MILLISECONDS);
????????????this.latch = null;
????????}
????????return?true;
????}
????public?void?unlock()?{
????????try?{
????????????System.out.println("unlock "?+ myZnode);
????????????zk.delete(myZnode,-1);
????????????myZnode = null;
????????????zk.close();
????????} catch?(InterruptedException e) {
????????????e.printStackTrace();
????????} catch?(KeeperException e) {
????????????e.printStackTrace();
????????}
????}
????public?void?lockInterruptibly()?throws?InterruptedException {
????????this.lock();
????}
????public?Condition newCondition()?{
????????return?null;
????}
????public?class?LockException?extends?RuntimeException?{
????????private?static?final?long?serialVersionUID = 1L;
????????public?LockException(String e){
????????????super(e);
????????}
????????public?LockException(Exception e){
????????????super(e);
????????}
????}
} 優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
有效的解決單點(diǎn)問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實(shí)現(xiàn)起來較為簡單。
缺點(diǎn):
性能上可能并沒有緩存服務(wù)那么高,因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過程中,都要?jiǎng)討B(tài)創(chuàng)建、銷毀臨時(shí)節(jié)點(diǎn)來實(shí)現(xiàn)鎖功能。ZK 中創(chuàng)建和刪除節(jié)點(diǎn)只能通過 Leader 服務(wù)器來執(zhí)行,然后將數(shù)據(jù)同步到所有的 Follower 機(jī)器上。還需要對(duì) ZK的原理有所了解。
基于 Consul 做分布式鎖
DD 寫過類似文章,其實(shí)主要利用 Consul 的 Key / Value 存儲(chǔ) API 中的 acquire 和 release 操作來實(shí)現(xiàn)。
文章地址:http://blog.didispace.com/spring-cloud-consul-lock-and-semphore/
使用分布式鎖的注意事項(xiàng)
1、注意分布式鎖的開銷
2、注意加鎖的粒度
3、加鎖的方式
總結(jié)
無論你身處一個(gè)什么樣的公司,最開始的工作可能都需要從最簡單的做起。不要提阿里和騰訊的業(yè)務(wù)場(chǎng)景 qps 如何大,因?yàn)樵谶@樣的大場(chǎng)景中你未必能親自參與項(xiàng)目,親自參與項(xiàng)目未必能是核心的設(shè)計(jì)者,是核心的設(shè)計(jì)者未必能獨(dú)自設(shè)計(jì)。希望大家能根據(jù)自己公司業(yè)務(wù)場(chǎng)景,選擇適合自己項(xiàng)目的方案。
粉絲福利:108本java從入門到大神精選電子書領(lǐng)取
???
?長按上方鋒哥微信二維碼?2 秒 備注「1234」即可獲取資料以及 可以進(jìn)入java1234官方微信群
感謝點(diǎn)贊支持下哈?
