什么是鎖?
在單進程的系統(tǒng)中,當(dāng)存在多個線程可以同時改變某個變量(可變共享變量)時祭玉,就需要對變量或代碼塊做同步胳嘲,使其在修改這種變量時能夠線性執(zhí)行消除并發(fā)修改變量。
而同步的本質(zhì)是通過鎖來實現(xiàn)的坛怪。為了實現(xiàn)多個線程在一個時刻同一個代碼塊只能有一個線程可執(zhí)行,那么需要在某個地方做個標(biāo)記,這個標(biāo)記必須每個線程都能看到谬晕,當(dāng)標(biāo)記不存在時可以設(shè)置該標(biāo)記,其余后續(xù)線程發(fā)現(xiàn)已經(jīng)有標(biāo)記了則等待擁有標(biāo)記的線程結(jié)束同步代碼塊取消標(biāo)記后再去嘗試設(shè)置標(biāo)記携取。這個標(biāo)記可以理解為鎖攒钳。
不同地方實現(xiàn)鎖的方式也不一樣,只要能滿足所有線程都能看得到標(biāo)記即可雷滋。如 Java 中 synchronize 是在對象頭設(shè)置標(biāo)記不撑,Lock 接口的實現(xiàn)類基本上都只是某一個 volitile 修飾的 int 型變量其保證每個線程都能擁有對該 int 的可見性和原子修改,linux 內(nèi)核中也是利用互斥量或信號量等內(nèi)存數(shù)據(jù)做標(biāo)記晤斩。
除了利用內(nèi)存數(shù)據(jù)做鎖其實任何互斥的都能做鎖(只考慮互斥情況)焕檬,如流水表中流水號與時間結(jié)合做冪等校驗可以看作是一個不會釋放的鎖,或者使用某個文件是否存在作為鎖等澳泵。只需要滿足在對標(biāo)記進行修改能保證原子性和內(nèi)存可見性即可实愚。
什么是分布式?
分布式的 CAP 理論告訴我們:
任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance)爆侣,最多只能同時滿足兩項萍程。
目前很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題兔仰∶8海基于 CAP理論,很多系統(tǒng)在設(shè)計之初就要對這三者做出取舍乎赴。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景中忍法,都需要犧牲強一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證最終一致性榕吼。
分布式場景
此處主要指集群模式下饿序,多個相同服務(wù)同時開啟.
在許多的場景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性羹蚣,需要很多的技術(shù)方案來支持原探,比如分布式事務(wù)、分布式鎖等顽素。很多時候我們需要保證一個方法在同一時間內(nèi)只能被同一個線程執(zhí)行咽弦。在單機環(huán)境中,通過 Java 提供的并發(fā) API 我們可以解決胁出,但是在分布式環(huán)境下型型,就沒有那么簡單啦。
分布式與單機情況下最大的不同在于其不是多線程而是多進程全蝶。
多線程由于可以共享堆內(nèi)存闹蒜,因此可以簡單的采取內(nèi)存作為標(biāo)記存儲位置。而進程之間甚至可能都不在同一臺物理機上抑淫,因此需要將標(biāo)記存儲在一個所有進程都能看到的地方绷落。
什么是分布式鎖?
當(dāng)在分布式模型下丈冬,數(shù)據(jù)只有一份(或有限制)嘱函,此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進程數(shù)甘畅。
與單機模式下的鎖不僅需要保證進程可見埂蕊,還需要考慮進程與鎖之間的網(wǎng)絡(luò)問題。(我覺得分布式情況下之所以問題變得復(fù)雜疏唾,主要就是需要考慮到網(wǎng)絡(luò)的延時和不可靠蓄氧。。槐脏。一個大坑)
分布式鎖還是可以將標(biāo)記存在內(nèi)存喉童,只是該內(nèi)存不是某個進程分配的內(nèi)存而是公共內(nèi)存如 Redis、Memcache顿天。至于利用數(shù)據(jù)庫堂氯、文件等做鎖與單機的實現(xiàn)是一樣的蔑担,只要保證標(biāo)記能互斥就行。
我們需要怎樣的分布式鎖咽白?
- 可以保證在分布式部署的應(yīng)用集群中啤握,同一個方法在同一時間只能被一臺機器上的一個線程執(zhí)行。
- 這把鎖要是一把可重入鎖(避免死鎖)
- 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
- 這把鎖最好是一把公平鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
- 有高可用的獲取鎖和釋放鎖功能
- 獲取鎖和釋放鎖的性能要好
一晶框、基于數(shù)據(jù)庫做分布式鎖
1排抬、基于樂觀鎖
(1)、基于表主鍵唯一做分布式鎖
利用主鍵唯一的特性授段,如果有多個請求同時提交到數(shù)據(jù)庫的話蹲蒲,數(shù)據(jù)庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖侵贵,當(dāng)方法執(zhí)行完畢之后届搁,想要釋放鎖的話,刪除這條數(shù)據(jù)庫記錄即可窍育。
上面這種簡單的實現(xiàn)有以下幾個問題:
- 這把鎖強依賴數(shù)據(jù)庫的可用性咖祭,數(shù)據(jù)庫是一個單點,一旦數(shù)據(jù)庫掛掉蔫骂,會導(dǎo)致業(yè)務(wù)系統(tǒng)不可用么翰。
- 這把鎖沒有失效時間,一旦解鎖操作失敗辽旋,就會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中浩嫌,其他線程無法再獲得到鎖。
- 這把鎖只能是非阻塞的补胚,因為數(shù)據(jù)的 insert 操作码耐,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進入排隊隊列溶其,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作骚腥。
- 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖瓶逃。因為數(shù)據(jù)中數(shù)據(jù)已經(jīng)存在了束铭。
- 這把鎖是非公平鎖,所有等待鎖的線程憑運氣去爭奪鎖厢绝。
- 在 MySQL 數(shù)據(jù)庫中采用主鍵沖突防重契沫,在大并發(fā)情況下有可能會造成鎖表現(xiàn)象。
當(dāng)然昔汉,我們也可以有其他方式解決上面的問題懈万。 - 數(shù)據(jù)庫是單點?搞兩個數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步会通,一旦掛掉快速切換到備庫上口予。
- 沒有失效時間?只要做一個定時任務(wù)涕侈,每隔一定時間把數(shù)據(jù)庫中的超時數(shù)據(jù)清理一遍苹威。
- 非阻塞的?搞一個 while 循環(huán)驾凶,直到 insert 成功再返回成功牙甫。
- 非重入的?在數(shù)據(jù)庫表中加個字段调违,記錄當(dāng)前獲得鎖的機器的主機信息和線程信息窟哺,那么下次再獲取鎖的時候先查詢數(shù)據(jù)庫,如果當(dāng)前機器的主機信息和線程信息在數(shù)據(jù)庫可以查到的話技肩,直接把鎖分配給他就可以了且轨。
- 非公平的?再建一張中間表虚婿,將等待鎖的線程全記錄下來旋奢,并根據(jù)創(chuàng)建時間排序,只有最先創(chuàng)建的允許獲取鎖然痊。比較好的辦法是在程序中生產(chǎn)主鍵進行防重至朗。
(2)、基于表字段版本號做分布式鎖
這個策略源于 mysql 的 mvcc 機制剧浸,使用這個策略其實本身沒有什么問題锹引,唯一的問題就是對數(shù)據(jù)表侵入較大,我們要為每個表設(shè)計一個版本號字段唆香,然后寫一條判斷 sql 每次進行判斷嫌变,增加了數(shù)據(jù)庫操作的次數(shù),在高并發(fā)的要求下躬它,對數(shù)據(jù)庫連接的開銷也是無法忍受的腾啥。
2、基于悲觀鎖
(1)冯吓、基于數(shù)據(jù)庫排他鎖做分布式鎖
在查詢語句后面增加for update倘待,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖 (注意: InnoDB 引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行級鎖桑谍,否則會使用表級鎖延柠。這里我們希望使用行級鎖,就要給要執(zhí)行的方法字段名添加索引锣披,值得注意的是,這個索引一定要創(chuàng)建成唯一索引,否則會出現(xiàn)多個重載方法之間無法同時被訪問的問題雹仿。重載方法的話建議把參數(shù)類型也加上增热。)。當(dāng)某條記錄被加上排他鎖之后胧辽,其他線程無法再在該行記錄上增加排他鎖峻仇。
我們可以認為獲得排他鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后邑商,可以執(zhí)行方法的業(yè)務(wù)邏輯摄咆,執(zhí)行完方法之后,通過connection.commit()操作來釋放鎖人断。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題吭从。
阻塞鎖 for update語句會在執(zhí)行成功后立即返回,在執(zhí)行失敗時一直處于阻塞狀態(tài)恶迈,直到成功涩金。
鎖定之后服務(wù)宕機,無法釋放暇仲?使用這種方式步做,服務(wù)宕機之后數(shù)據(jù)庫會自己把鎖釋放掉。
但是還是無法直接解決數(shù)據(jù)庫單點和可重入問題奈附。
這里還可能存在另外一個問題全度,雖然我們對方法字段名使用了唯一索引,并且顯示使用 for update 來使用行級鎖斥滤。但是讼载,MySQL 會對查詢進行優(yōu)化,即便在條件中使用了索引字段中跌,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計劃的代價來決定的咨堤,如果 MySQL 認為全表掃效率更高,比如對一些很小的表漩符,它就不會使用索引一喘,這種情況下 InnoDB 將使用表鎖,而不是行鎖嗜暴。如果發(fā)生這種情況就悲劇了凸克。。闷沥。
還有一個問題萎战,就是我們要使用排他鎖來進行分布式鎖的 lock,那么一個排他鎖長時間不提交舆逃,就會占用數(shù)據(jù)庫連接蚂维。一旦類似的連接變得多了戳粒,就可能把數(shù)據(jù)庫連接池撐爆。
優(yōu)缺點
優(yōu)點:簡單虫啥,易于理解
缺點:會有各種各樣的問題(操作數(shù)據(jù)庫需要一定的開銷蔚约,使用數(shù)據(jù)庫的行級鎖并不一定靠譜,性能不靠譜)
二涂籽、基于 Redis 做分布式鎖
1苹祟、基于 redis 的 setnx()、expire() 方法做分布式鎖
setnx()
setnx 的含義就是 SET if Not Exists评雌,其主要有兩個參數(shù) setnx(key, value)树枫。該方法是原子的,如果 key 不存在景东,則設(shè)置當(dāng)前 key 成功砂轻,返回 1耐薯;如果當(dāng)前 key 已經(jīng)存在舔清,則設(shè)置當(dāng)前 key 失敗,返回 0曲初。
expire()
expire 設(shè)置過期時間体谒,要注意的是 setnx 命令不能設(shè)置 key 的超時時間,只能通過 expire() 來對 key 設(shè)置臼婆。
使用步驟:
- setnx(lockkey, 1) 如果返回 0抒痒,則說明占位失敗颁褂;如果返回 1故响,則說明占位成功
- expire() 命令對 lockkey 設(shè)置超時時間,為的是避免死鎖問題颁独。
- 執(zhí)行完業(yè)務(wù)代碼后彩届,可以通過 delete 命令刪除 key。
這個方案其實是可以解決日常工作中的需求的誓酒,但從技術(shù)方案的探討上來說樟蠕,可能還有一些可以完善的地方。比如靠柑,如果在第一步 setnx 執(zhí)行成功后寨辩,在 expire() 命令執(zhí)行成功前,發(fā)生了宕機的現(xiàn)象歼冰,那么就依然會出現(xiàn)死鎖的問題靡狞,所以如果要對其進行完善的話,可以使用 redis 的 setnx()隔嫡、get() 和 getset() 方法來實現(xiàn)分布式鎖甸怕。
2甘穿、基于 redis 的 setnx()、get()蕾各、getset()方法做分布式鎖
這個方案的背景主要是在 setnx() 和 expire() 的方案上針對可能存在的死鎖問題扒磁,做了一些優(yōu)化庆揪。
getset()
這個命令主要有兩個參數(shù) getset(key式曲,newValue)。該方法是原子的缸榛,對 key 設(shè)置 newValue 這個值吝羞,并且返回 key 原來的舊值。假設(shè) key 原來是不存在的内颗,那么多次執(zhí)行這個命令钧排,會出現(xiàn)下邊的效果:
getset(key, “value1”) 返回 null 此時 key 的值會被設(shè)置為 value1
getset(key, “value2”) 返回 value1 此時 key 的值會被設(shè)置為 value2
依次類推!
使用步驟:
- setnx(lockkey, 當(dāng)前時間+過期超時時間)均澳,如果返回 1恨溜,則獲取鎖成功;如果返回 0 則沒有獲取到鎖找前,轉(zhuǎn)向 2糟袁。
- get(lockkey) 獲取值 oldExpireTime ,并將這個 value 值與當(dāng)前的系統(tǒng)時間進行比較躺盛,如果小于當(dāng)前系統(tǒng)時間项戴,則認為這個鎖已經(jīng)超時,可以允許別的請求重新獲取槽惫,轉(zhuǎn)向 3周叮。
- 計算 newExpireTime = 當(dāng)前時間+過期超時時間,然后 getset(lockkey, newExpireTime) 會返回當(dāng)前 lockkey 的值currentExpireTime界斜。
- 判斷 currentExpireTime 與 oldExpireTime 是否相等仿耽,如果相等,說明當(dāng)前 getset 設(shè)置成功各薇,獲取到了鎖项贺。如果不相等,說明這個鎖又被別的請求獲取走了得糜,那么當(dāng)前請求可以直接返回失敗敬扛,或者繼續(xù)重試。
- 在獲取到鎖之后朝抖,當(dāng)前線程可以開始自己的業(yè)務(wù)處理啥箭,當(dāng)處理完畢后,比較自己的處理時間和對于鎖設(shè)置的超時時間治宣,如果小于鎖設(shè)置的超時時間急侥,則直接執(zhí)行 delete 釋放鎖砌滞;如果大于鎖設(shè)置的超時時間,則不需要再鎖進行處理坏怪。
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 過期時間贝润,單位秒
* @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 過期時間打掘,單位秒
* @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()) {
//超時
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)取獎勵");
}
}
3尊蚁、基于 Redlock 做分布式鎖
Redlock 是 Redis 的作者 antirez 給出的集群模式的 Redis 分布式鎖,它基于 N 個完全獨立的 Redis 節(jié)點(通常情況下 N 可以設(shè)置成 5)侣夷。
算法的步驟如下:
- 客戶端獲取當(dāng)前時間横朋,以毫秒為單位。
- 客戶端嘗試獲取 N 個節(jié)點的鎖百拓,(每個節(jié)點獲取鎖的方式和前面說的緩存鎖一樣)琴锭,N 個節(jié)點以相同的 key 和 value 獲取鎖⊙么客戶端需要設(shè)置接口訪問超時决帖,接口超時時間需要遠遠小于鎖超時時間,比如鎖自動釋放的時間是 10s粪牲,那么接口超時大概設(shè)置 5-50ms古瓤。這樣可以在有 redis 節(jié)點宕機后,訪問該節(jié)點時能盡快超時腺阳,而減小鎖的正常使用落君。
- 客戶端計算在獲得鎖的時候花費了多少時間,方法是用當(dāng)前時間減去在步驟一獲取的時間亭引,只有客戶端獲得了超過 3 個節(jié)點的鎖绎速,而且獲取鎖的時間小于鎖的超時時間,客戶端才獲得了分布式鎖焙蚓。
- 客戶端獲取的鎖的時間為設(shè)置的鎖超時時間減去步驟三計算出的獲取鎖花費時間纹冤。
- 如果客戶端獲取鎖失敗了,客戶端會依次刪除所有的鎖购公。
使用 Redlock 算法萌京,可以保證在掛掉最多 2 個節(jié)點的時候,分布式鎖服務(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)缺點
優(yōu)點:
性能高
缺點:
失效時間設(shè)置多長時間為好制恍?如何設(shè)置的失效時間太短父能,方法沒等執(zhí)行完,鎖就自動釋放了净神,那么就會產(chǎn)生并發(fā)問題何吝。如果設(shè)置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間强挫。
4岔霸、基于 redisson 做分布式鎖
redisson 是 redis 官方的分布式鎖組件薛躬。GitHub 地址:https://github.com/redisson/redisson
上面的這個問題 ——> 失效時間設(shè)置多長時間為好俯渤?這個問題在 redisson 的做法是:每獲得一個鎖時,只設(shè)置一個很短的超時時間型宝,同時起一個線程在每次快要到超時時間時去刷新鎖的超時時間八匠。在釋放鎖的同時結(jié)束這個線程。
三趴酣、基于 ZooKeeper 做分布式鎖
zookeeper 鎖相關(guān)基礎(chǔ)知識
zk 一般由多個節(jié)點構(gòu)成(單數(shù))梨树,采用 zab 一致性協(xié)議。因此可以將 zk 看成一個單點結(jié)構(gòu)岖寞,對其修改數(shù)據(jù)其內(nèi)部自動將所有節(jié)點數(shù)據(jù)進行修改而后才提供查詢服務(wù)抡四。
zk 的數(shù)據(jù)以目錄樹的形式,每個目錄稱為 znode仗谆, znode 中可存儲數(shù)據(jù)(一般不超過 1M)指巡,還可以在其中增加子節(jié)點。
子節(jié)點有三種類型隶垮。序列化節(jié)點藻雪,每在該節(jié)點下增加一個節(jié)點自動給該節(jié)點的名稱上自增。臨時節(jié)點狸吞,一旦創(chuàng)建這個 znode 的客戶端與服務(wù)器失去聯(lián)系勉耀,這個 znode 也將自動刪除。最后就是普通節(jié)點蹋偏。
Watch 機制便斥,client 可以監(jiān)控每個節(jié)點的變化,當(dāng)產(chǎn)生變化會給 client 產(chǎn)生一個事件威始。
zk 基本鎖
原理:利用臨時節(jié)點與 watch 機制枢纠。每個鎖占用一個普通節(jié)點 /lock,當(dāng)需要獲取鎖時在 /lock 目錄下創(chuàng)建一個臨時節(jié)點字逗,創(chuàng)建成功則表示獲取鎖成功京郑,失敗則 watch/lock 節(jié)點宅广,有刪除操作后再去爭鎖。臨時節(jié)點好處在于當(dāng)進程掛掉后能自動上鎖的節(jié)點自動刪除即取消鎖些举。
缺點:所有取鎖失敗的進程都監(jiān)聽父節(jié)點跟狱,很容易發(fā)生羊群效應(yīng),即當(dāng)釋放鎖后所有等待進程一起來創(chuàng)建節(jié)點户魏,并發(fā)量很大驶臊。
zk 鎖優(yōu)化
原理:上鎖改為創(chuàng)建臨時有序節(jié)點,每個上鎖的節(jié)點均能創(chuàng)建節(jié)點成功叼丑,只是其序號不同关翎。只有序號最小的可以擁有鎖,如果這個節(jié)點序號不是最小的則 watch 序號比本身小的前一個節(jié)點 (公平鎖)鸠信。
步驟:
- 在 /lock 節(jié)點下創(chuàng)建一個有序臨時節(jié)點 (EPHEMERAL_SEQUENTIAL)纵寝。
- 判斷創(chuàng)建的節(jié)點序號是否最小,如果是最小則獲取鎖成功星立。不是則取鎖失敗爽茴,然后 watch 序號比本身小的前一個節(jié)點。(避免很多線程watch同一個node绰垂,導(dǎo)致羊群效應(yīng))
- 當(dāng)取鎖失敗室奏,設(shè)置 watch 后則等待 watch 事件到來后,再次判斷是否序號最小劲装。
- 取鎖成功則執(zhí)行代碼胧沫,最后釋放鎖(刪除該節(jié)點)。
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;//等待前一個鎖
private String myZnode;//當(dāng)前鎖
private CountDownLatch latch;//計數(shù)器
private int sessionTimeout = 30000;
private List<Exception> exception = new ArrayList<Exception>();
/**
* 創(chuàng)建分布式鎖,使用前請確認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)建一個與服務(wù)器的連接
try {
zk = new ZooKeeper(config, sessionTimeout, this);
Stat stat = zk.exists(root, false);
if(stat == null){
// 創(chuàng)建根節(jié)點
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é)點的監(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)建臨時子節(jié)點
myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(myZnode + " is created ");
//取出所有子節(jié)點
List<String> subNodes = zk.getChildren(root, false);
//取出所有l(wèi)ockName的鎖
List<String> lockObjNodes = new ArrayList<String>();
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é)點,則表示取得鎖
return true;
}
//如果不是最小的節(jié)點占业,找到比自己小1的節(jié)點
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);
//判斷比自己小一個數(shù)的節(jié)點是否存在,如果不存在則無需等待鎖,同時注冊監(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)缺點
優(yōu)點:
有效的解決單點問題绒怨,不可重入問題,非阻塞問題以及鎖無法釋放的問題纺酸。實現(xiàn)起來較為簡單窖逗。
缺點:
性能上可能并沒有緩存服務(wù)那么高,因為每次在創(chuàng)建鎖和釋放鎖的過程中餐蔬,都要動態(tài)創(chuàng)建碎紊、銷毀臨時節(jié)點來實現(xiàn)鎖功能。ZK 中創(chuàng)建和刪除節(jié)點只能通過 Leader 服務(wù)器來執(zhí)行樊诺,然后將數(shù)據(jù)同步到所有的 Follower 機器上仗考。還需要對 ZK的原理有所了解。
四词爬、基于 Consul 做分布式鎖
DD 寫過類似文章秃嗜,其實主要利用 Consul 的 Key / Value 存儲 API 中的 acquire 和 release 操作來實現(xiàn)。
文章地址:http://blog.didispace.com/spring-cloud-consul-lock-and-semphore/
五、使用分布式鎖的注意事項
1锅锨、注意分布式鎖的開銷
2叽赊、注意加鎖的粒度
3、加鎖的方式
六必搞、分布式可重入鎖的設(shè)計
需記錄機器線程id(MAC地址 + jvm進程ID + 線程ID)和重入次數(shù)
七必指、總結(jié)
無論你身處一個什么樣的公司,最開始的工作可能都需要從最簡單的做起恕洲。不要提阿里和騰訊的業(yè)務(wù)場景 qps 如何大塔橡,因為在這樣的大場景中你未必能親自參與項目,親自參與項目未必能是核心的設(shè)計者霜第,是核心的設(shè)計者未必能獨自設(shè)計葛家。希望大家能根據(jù)自己公司業(yè)務(wù)場景,選擇適合自己項目的方案泌类。
參考資料
http://www.hollischuang.com/archives/1716
http://www.spring4all.com/question/158
https://www.cnblogs.com/PurpleDream/p/5559352.html
http://www.cnblogs.com/PurpleDream/p/5573040.html
https://www.cnblogs.com/suolu/p/6588902.html