本文導(dǎo)讀:
1、引入業(yè)務(wù)場景
2屠阻、分布式鎖家族成員介紹
3红省、分布式鎖成員實(shí)現(xiàn)原理剖析
4、最后的總結(jié)
2019 已經(jīng)過去国觉!
2020 已經(jīng)到站吧恃!
1引入業(yè)務(wù)場景
首先來由一個(gè)場景引入:
最近老板接了一個(gè)大單子,允許在某終端設(shè)備安裝我們的APP麻诀,終端設(shè)備廠商日活起碼得幾十萬到百萬級(jí)別痕寓,這個(gè)APP也是近期產(chǎn)品根據(jù)市場競品分析設(shè)計(jì)出來的,幾個(gè)小碼農(nóng)通宵達(dá)旦開發(fā)出來的蝇闭,主要功能是在線購物一站式服務(wù)呻率,后臺(tái)可以給各個(gè)商家分配權(quán)限,來維護(hù)需要售賣的商品信息呻引。
老板大O:談下來不容易礼仗,接下來就是考慮如何吸引終端設(shè)備上更多的用戶注冊(cè)上來,如何引導(dǎo)用戶購買逻悠,這塊就交給小P去負(fù)責(zé)了元践,需求盡快做,我明天出差童谒!
產(chǎn)品小P:嘿嘿~单旁,眼珠一轉(zhuǎn)兒,很容易就想到了饥伊,心里想:“這還不簡單象浑,起碼在首頁搞個(gè)活動(dòng)頁... ”。
技術(shù)小T:很快了解了產(chǎn)品的需求撵渡,目前小J主要負(fù)責(zé)這塊融柬,找了前端和后端同學(xué)一起將活動(dòng)頁搞的快差不多了。
業(yè)務(wù)場景一出現(xiàn):
因?yàn)樾剛接手項(xiàng)目趋距,正在吭哧吭哧對(duì)熟悉著代碼粒氧、部署架構(gòu)。在看代碼過程中發(fā)現(xiàn)节腐,下單這塊代碼可能會(huì)出現(xiàn)問題外盯,這可是分布式部署的,如果多個(gè)用戶同時(shí)購買同一個(gè)商品翼雀,就可能導(dǎo)致商品出現(xiàn) 庫存超賣 (數(shù)據(jù)不一致)
現(xiàn)象饱苟,對(duì)于這種情況代碼中并沒有做任何控制。
原來一問才知道狼渊,以前他們都是售賣的虛擬商品箱熬,沒啥庫存一說类垦,所以當(dāng)時(shí)沒有考慮那么多...
這次不一樣啊,這次是售賣的實(shí)體商品城须,那就有庫存這么一說了蚤认,起碼要保證不能超過庫存設(shè)定的數(shù)量吧。
小T大眼對(duì)著屏幕糕伐,屏住呼吸砰琢,還好提前發(fā)現(xiàn)了這個(gè)問題,趕緊想辦法修復(fù)良瞧,不賺錢還賠錢陪汽,老板不得瘋了,還想不想干了~
業(yè)務(wù)場景二出現(xiàn):
小T下面的一位兄弟正在壓測(cè)褥蚯,發(fā)現(xiàn)個(gè)小問題挚冤,因?yàn)樵诮K端設(shè)備上跟鵝廠有緊密合作,調(diào)用他們的接口時(shí)需要獲取到access_token遵岩,但是這個(gè)access_token過期時(shí)間是2小時(shí)你辣,過期后需要重新獲取。
壓測(cè)時(shí)發(fā)現(xiàn)當(dāng)?shù)竭_(dá)過期時(shí)間時(shí)尘执,日志看刷出來好幾個(gè)不一樣的access_token舍哄,因?yàn)檫@個(gè)服務(wù)也是分布式部署的,多個(gè)節(jié)點(diǎn)同時(shí)發(fā)起了第三方接口請(qǐng)求導(dǎo)致誊锭。
雖然以最后一次獲取的access_token為準(zhǔn)表悬,也沒什么不良副作用,但是會(huì)導(dǎo)致多次不必要的對(duì)第三方接口的調(diào)用丧靡,也會(huì)短時(shí)間內(nèi)造成access_token的 重復(fù)無效獲润∧(重復(fù)工作)
。
業(yè)務(wù)場景三出現(xiàn):
下單完成后温治,還要通知倉儲(chǔ)物流饭庞,待用戶支付完成,支付回調(diào)有可能會(huì)將多條訂單消息發(fā)送到MQ熬荆,倉儲(chǔ)服務(wù)會(huì)從MQ消費(fèi)訂單消息舟山,此時(shí)就要 保證冪等性
,對(duì)訂單消息做 去重
處理卤恳。
以上便于大家理解為什么要用分布式鎖才能解決累盗,勾勒出的幾個(gè)業(yè)務(wù)場景。
上面的問題無一例外突琳,都是針對(duì)共享資源要求串行化處理若债,才能保證安全且合理的操作。
用一張圖來體驗(yàn)一下:
此時(shí)拆融,使用Java提供的Synchronized蠢琳、ReentrantLock啊终、ReentrantReadWriteLock...,僅能在單個(gè)JVM進(jìn)程內(nèi)對(duì)多線程對(duì)共享資源保證線程安全挪凑,在分布式系統(tǒng)環(huán)境下統(tǒng)統(tǒng)都不好使孕索,心情是不是拔涼呀。
這個(gè)問題得請(qǐng)教 分布式鎖
家族來支持一下躏碳,聽說他們家族內(nèi)有很多成員,每個(gè)成員都有這個(gè)分布式鎖功能散怖,接下來就開始探索一下菇绵。
2分布式鎖家族成員介紹
為什么需要分布式鎖才能解決?
聽聽 Martin 大佬們給出的說法:
Martin kleppmann 是英國劍橋大學(xué)的分布式系統(tǒng)的研究員镇眷,曾經(jīng)跟 Redis 之父 Antirez 進(jìn)行過關(guān)于 RedLock (Redis里分布式鎖的實(shí)現(xiàn)算法)是否安全的激烈討論咬最。
他們討論了啥,整急眼了欠动? 都能單獨(dú)寫篇文章了
請(qǐng)你自己看 Maritin 博客文章:
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
效率:
使用分布式鎖可以避免多個(gè)客戶端重復(fù)相同的工作永乌,這些工作會(huì)浪費(fèi)資源。比如用戶支付完成后具伍,可能會(huì)收到多次短信或郵件提醒翅雏。
比如業(yè)務(wù)場景二,重復(fù)獲取access_token人芽。
對(duì)共享資源的操作是冪等性操作望几,無論你操作多少次都不會(huì)出現(xiàn)不同結(jié)果。本質(zhì)上就是為了避免對(duì)共享資源重復(fù)操作萤厅,從而提高效率橄抹。
正確性:
使用分布式鎖同樣可以避免鎖失效的發(fā)生,一旦發(fā)生會(huì)引起正確性的破壞惕味,可能會(huì)導(dǎo)致數(shù)據(jù)不一致楼誓,數(shù)據(jù)缺失或者其他嚴(yán)重的問題。
比如業(yè)務(wù)場景一名挥,商品庫存超賣問題疟羹。
對(duì)共享資源的操作是非冪等性操作,多個(gè)客戶端操作共享資源會(huì)導(dǎo)致數(shù)據(jù)不一致躺同。
分布式鎖有哪些特點(diǎn)呢阁猜?
以下是分布式鎖的一些特點(diǎn),分布式鎖家族成員并不一定都滿足這個(gè)要求蹋艺,實(shí)現(xiàn)機(jī)制不大一樣剃袍。
互斥性: 分布式鎖要保證在多個(gè)客戶端之間的互斥。
可重入性:同一客戶端的相同線程捎谨,允許重復(fù)多次加鎖民效。
鎖超時(shí):和本地鎖一樣支持鎖超時(shí)憔维,防止死鎖。
非阻塞: 能與 ReentrantLock 一樣支持 trylock() 非阻塞方式獲得鎖畏邢。悲觀鎖就是阻塞的业扒,樂觀鎖和redis鎖不是阻塞的。
支持公平鎖和非公平鎖:公平鎖是指按照請(qǐng)求加鎖的順序獲得鎖舒萎,非公平鎖真好相反請(qǐng)求加鎖是無序的程储。
分布式鎖家族實(shí)現(xiàn)者介紹
分布式鎖家族實(shí)現(xiàn)者一覽:
思維導(dǎo)圖做了一個(gè)簡單分類,不一定特別準(zhǔn)確臂寝,幾乎包含了分布式鎖各個(gè)組件實(shí)現(xiàn)者章鲤。
下面讓他們分別來做下自我介紹:
1、數(shù)據(jù)庫
排它鎖(悲觀鎖):基于 select * from table where xx=yy for update
SQL語句來實(shí)現(xiàn)咆贬,有很多缺陷败徊,一般不推薦使用,后文介紹掏缎。
樂觀鎖:表中添加一個(gè)時(shí)間戳或者版本號(hào)的字段來實(shí)現(xiàn)皱蹦,update xx set version = new... where id = y and version = old
當(dāng)更新不成功,客戶端重試眷蜈,重新讀取最新的版本號(hào)或時(shí)間戳沪哺,再次嘗試更新,類似 CAS
機(jī)制端蛆,推薦使用凤粗。
2、Redis
特點(diǎn):CAP模型屬于AP | 無一致性算法 | 性能好
開發(fā)常用今豆,如果你的項(xiàng)目中正好使用了redis嫌拣,不想引入額外的分布式鎖組件,推薦使用呆躲。
業(yè)界也提供了多個(gè)現(xiàn)成好用的框架予以支持分布式鎖异逐,比如Redisson、spring-integration-redis插掂、redis自帶的setnx命令灰瞻,推薦直接使用。
另外辅甥,可基于redis命令和redis lua支持的原子特性酝润,自行實(shí)現(xiàn)分布式鎖。
3璃弄、Zookeeper
特點(diǎn):CAP模型屬于CP | ZAB一致性算法實(shí)現(xiàn) | 穩(wěn)定性好
開發(fā)常用要销,如果你的項(xiàng)目中正好使用了zk集群,推薦使用夏块。
業(yè)界有Apache Curator框架提供了現(xiàn)成的分布式鎖功能疏咐,現(xiàn)成的纤掸,推薦直接使用。
另外浑塞,可基于Zookeeper自身的特性和原生Zookeeper API自行實(shí)現(xiàn)分布式鎖借跪。
4、其他
Chubby酌壕,Google開發(fā)的粗粒度分布鎖的服務(wù)掏愁,但是并沒有開源,開放出了論文和一些相關(guān)文檔可以進(jìn)一步了解仅孩,出門百度一下獲取文檔托猩,不做過多討論。
Tair辽慕,是阿里開源的一個(gè)分布式KV存儲(chǔ)方案,沒有用過赦肃,不做過多討論溅蛉。
Etcd,CAP模型中屬于CP他宛,Raft一致性算法實(shí)現(xiàn)船侧,沒有用過,不做過多討論厅各。
Hazelcast镜撩,是基于內(nèi)存的數(shù)據(jù)網(wǎng)格開源項(xiàng)目,提供彈性可擴(kuò)展的分布式內(nèi)存計(jì)算队塘,并且被公認(rèn)是提高應(yīng)用程序性能和擴(kuò)展性最好的方案袁梗,聽上去很牛逼,但是沒用過憔古,不做過多討論遮怜。
當(dāng)然了,上面推薦的常用分布式鎖Zookeeper和Redis鸿市,使用時(shí)還需要根據(jù)具體的業(yè)務(wù)場景锯梁,做下權(quán)衡,實(shí)現(xiàn)功能上都能達(dá)到你要的效果焰情,原理上有很大的不同陌凳。
畫外音: 你對(duì)哪個(gè)熟悉,原理也都了解内舟,hold住合敦,你就用哪個(gè)。
3 分布式鎖成員實(shí)現(xiàn)原理剖析!
數(shù)據(jù)庫悲觀鎖實(shí)現(xiàn)
以「悲觀的心態(tài)」操作資源谒获,無法獲得鎖成功蛤肌,就一直阻塞
著等待壁却。
1、有一張資源鎖表
CREATE TABLE `resource_lock` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的資源名',
`owner` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖擁有者',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時(shí)間裸准,自動(dòng)生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的資源';
resource_name字段必須有唯一索引展东。
2、使用姿勢(shì)
悲觀鎖必須添加事務(wù)
炒俱,在事務(wù)中實(shí)現(xiàn)盐肃。查詢和更新操作保證原子性,在一個(gè)事務(wù)里完成权悟。
偽代碼實(shí)現(xiàn):
@Transaction
public void lock(String name) {
ResourceLock rlock = exeSql("select * from resource_lock where resource_name = name for update");
if (rlock != null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}
在事務(wù)中使用 for update
鎖定的資源砸王。如果獲得鎖,會(huì)立即返回峦阁,執(zhí)行插入數(shù)據(jù)庫谦铃,后續(xù)再執(zhí)行一些其他業(yè)務(wù)邏輯,直到事務(wù)提交榔昔,然后釋放鎖驹闰;如果執(zhí)沒有獲得鎖,就會(huì)一直阻塞
著撒会,直到獲得鎖嘹朗。
你也可以在數(shù)據(jù)庫客戶端工具上測(cè)試出來這個(gè)效果,當(dāng)在一個(gè)終端執(zhí)行了 for update诵肛,不提交事務(wù)屹培。在另外的終端上執(zhí)行相同條件的 for update,會(huì)一直卡著怔檩,轉(zhuǎn)圈圈...
雖然也能實(shí)現(xiàn)分布式鎖的效果褪秀,但是會(huì)存在性能瓶頸。一般不會(huì)使用珠洗。
3溜歪、悲觀鎖優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡單易用,好理解许蓖,保障數(shù)據(jù)強(qiáng)一致性蝴猪。
缺點(diǎn)一大堆,羅列一下:
1)在 RR 事務(wù)級(jí)別膊爪,select 的 for update 操作是基于間隙鎖(gap lock)
實(shí)現(xiàn)的自阱,是一種悲觀鎖的實(shí)現(xiàn)方式,存在阻塞問題
米酬。這是個(gè)很嚴(yán)重的問題沛豌,阻塞也就代表著進(jìn)程阻塞,如果有10個(gè)進(jìn)程,程序阻塞1s加派,有就代表qps為10/s叫确。
2)高并發(fā)情況下,大量請(qǐng)求進(jìn)來芍锦,會(huì)導(dǎo)致大部分請(qǐng)求進(jìn)行排隊(duì)竹勉,影響數(shù)據(jù)庫穩(wěn)定性,也會(huì)耗費(fèi)
服務(wù)的CPU等資源
娄琉。
當(dāng)獲得鎖的客戶端等待時(shí)間過長時(shí)次乓,會(huì)提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并發(fā)情況下,也會(huì)造成占用過多的應(yīng)用線程孽水,導(dǎo)致業(yè)務(wù)無法正常響應(yīng)票腰。
3)如果優(yōu)先獲得鎖的線程因?yàn)槟承┰颍恢睕]有釋放掉鎖女气,可能會(huì)導(dǎo)致死鎖
的發(fā)生杏慰。如果導(dǎo)致死鎖的話,后續(xù)的所有操作都會(huì)阻塞炼鞠。
4)鎖的長時(shí)間不釋放逃默,會(huì)一直占用數(shù)據(jù)庫連接,導(dǎo)致數(shù)據(jù)庫連接不會(huì)釋放簇搅。如果高并發(fā)情況下可能會(huì)將數(shù)據(jù)庫連接池?fù)伪?/code>,影響其他服務(wù)软吐。
- MySql數(shù)據(jù)庫會(huì)做查詢優(yōu)化瘩将,即便使用了索引,優(yōu)化時(shí)如果發(fā)現(xiàn)全表掃效率更高凹耙,則可能會(huì)將行鎖升級(jí)為表鎖姿现,此時(shí)可能就更悲劇了。
6)不支持可重入特性肖抱,并且超時(shí)等待時(shí)間是全局的备典,不能隨便改動(dòng)。
數(shù)據(jù)庫樂觀鎖實(shí)現(xiàn)
樂觀鎖意述,以「樂觀的心態(tài)」來操作共享資源提佣,無法獲得鎖成功,沒關(guān)系過一會(huì)重試一下看看唄荤崇,再不行就直接退出拌屏,嘗試一定次數(shù)還是不行?也可以以后再說术荤,不用一直阻塞等著倚喂。
1、有一張資源表
為表添加一個(gè)字段瓣戚,版本號(hào)或者時(shí)間戳都可以端圈。通過版本號(hào)或者時(shí)間戳焦读,來保證多線程同時(shí)間操作共享資源的有序性和正確性。
CREATE TABLE `resource` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '資源名',
`share` varchar(64) NOT NULL DEFAULT '' COMMENT '狀態(tài)',
`version` int(4) NOT NULL DEFAULT '' COMMENT '版本號(hào)',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時(shí)間舱权,自動(dòng)生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='資源';
2矗晃、使用姿勢(shì)
樂觀鎖的代碼不能放到事務(wù)中執(zhí)行
,因?yàn)楫?dāng)更新不成功時(shí),客戶端需要重新讀取最新的版本號(hào)或時(shí)間戳刑巧,再次嘗試更新喧兄,如果放到事務(wù)中的話(事務(wù)隔離級(jí)別為repeatable read),每次重新讀取的版本號(hào)或者時(shí)間戳都是相同的,因?yàn)樵趓epeatable read隔離級(jí)別下是不可重復(fù)讀的啊楚。偽代碼實(shí)現(xiàn):
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
while(!succ)
{
//一直重試
Resrouce resource = exeSql("select * from resource where resource_name = xxx");
boolean succ = exeSql("update resource set version= 'newVersion' ... where resource_name = xxx and version = 'oldVersion'");
}
while循環(huán)不斷重試吠冤,如果更新失敗,則代表版本號(hào)不一致恭理,需要重新獲取新的版本號(hào)拯辙,直到更新成功。
3颜价、樂觀鎖優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡單易用涯保,保障數(shù)據(jù)一致性。
缺點(diǎn):
1)加行鎖的性能上有一定的開銷周伦。update時(shí)會(huì)加上排他鎖夕春。
2)高并發(fā)場景下,線程內(nèi)的while循環(huán)會(huì)耗費(fèi)一定的CPU資源专挪。
另外及志,比如在更新數(shù)據(jù)狀態(tài)的一些場景下,不考慮冪等性的情況下寨腔,可以直接利用 行鎖
來保證數(shù)據(jù)一致性速侈,示例:update table set state = 1 where id = xxx and state = 0;
樂觀鎖就類似 CAS
Compare And Swap更新機(jī)制,推薦閱讀:一文徹底搞懂CAS實(shí)現(xiàn)原理
基于Redis分布式鎖實(shí)現(xiàn)
基于SetNX實(shí)現(xiàn)分布式鎖
基于Redis實(shí)現(xiàn)的分布式鎖迫卢,性能上是最好的倚搬,實(shí)現(xiàn)上也是最復(fù)雜的。
前文中提到的 RedLock 是 Redis 之父 Antirez 提出來的分布式鎖的一種 「健壯」 的實(shí)現(xiàn)算法乾蛤,但爭議也較多每界,一般不推薦使用。
Redis 2.6.12 之前的版本中采用 setnx + expire 方式實(shí)現(xiàn)分布式鎖幻捏,示例代碼如下所示:
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
//設(shè)置鎖
if (result == 1) {
//獲取鎖成功
//若在這里程序突然崩潰盆犁,則無法設(shè)置過期時(shí)間,將發(fā)生死鎖
//通過過期時(shí)間刪除鎖
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
如果 lockKey 存在篡九,則返回失敗谐岁,否則返回成功。設(shè)置成功之后,為了能在完成同步代碼之后成功釋放鎖伊佃,方法中使用 expire() 方法給 lockKey 設(shè)置一個(gè)過期時(shí)間窜司,確認(rèn) key 值刪除,避免出現(xiàn)鎖無法釋放航揉,導(dǎo)致下一個(gè)線程無法獲取到鎖塞祈,即死鎖問題。
但是 setnx + expire 兩個(gè)命令放在程序里執(zhí)行帅涂,不是原子操作议薪,容易出事。
如果程序設(shè)置鎖之后媳友,此時(shí)斯议,在設(shè)置過期時(shí)間之前,程序崩潰了醇锚,將會(huì)出現(xiàn)死鎖問題
哼御,該鎖將不會(huì)釋放。
解決以上問題 焊唬,有兩個(gè)辦法:
1)方式一:lua腳本
我們也可以通過 Lua 腳本來實(shí)現(xiàn)鎖的設(shè)置和過期時(shí)間的原子性恋昼,再通過 redis.eval() 方法運(yùn)行該腳本
2)方式二:set原生命令
在 Redis 2.6.12 版本后 SETNX 增加了過期時(shí)間參數(shù):
SET lockKey anystring NX PX max-lock-time
雖然 SETNX 方式能夠保證設(shè)置鎖和過期時(shí)間的原子性,但是如果我們?cè)O(shè)置的過期時(shí)間比較短赶促,而執(zhí)行業(yè)務(wù)時(shí)間比較長液肌,就會(huì)存在鎖代碼塊失效的問題,此時(shí)其他請(qǐng)求獲得鎖鸥滨,同樣會(huì)有之前的問題矩屁。
我們需要將過期時(shí)間設(shè)置得足夠長,來保證以上問題不會(huì)出現(xiàn)爵赵,但是設(shè)置多長時(shí)間合理,也需要依具體業(yè)務(wù)來權(quán)衡泊脐。如果其他客戶端必須要阻塞拿到鎖空幻,需要設(shè)計(jì)循環(huán)超時(shí)等待機(jī)制等問題,感覺還挺麻煩的是吧良姆。
Spring企業(yè)集成模式實(shí)現(xiàn)分布式鎖
這節(jié)我們只做了解即可浦译。
除了使用Jedis客戶端之外木人,完全可以直接用Spring官方提供的企業(yè)集成模式
框架,里面提供了很多分布式鎖的方式但两,Spring提供了一個(gè)統(tǒng)一的分布式鎖抽象,具體實(shí)現(xiàn)目前支持:
Gemfire
Jdbc
Zookeeper
Redis
早期供置,分布式鎖的相關(guān)代碼存在于Spring Cloud的子項(xiàng)目Spring Cloud Cluster中谨湘,后來被遷到Spring Integration中。
Spring Integration 項(xiàng)目地址 :https://github.com/spring-projects/spring-integration
Spring強(qiáng)大之處在于此,對(duì)Lock
分布式鎖做了全局抽象紧阔。
抽象結(jié)構(gòu)如下所示:
LockRegistry
作為頂層抽象接口:
/**
* Strategy for maintaining a registry of shared locks
*
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.1.1
*/
@FunctionalInterface
public interface LockRegistry{
/**
* Obtains the lock associated with the parameter object.
* @param lockKey The object with which the lock is associated.
* @return The associated lock.
*/
Lock obtain(Object lockKey);
}
定義的 obtain()
方法獲得具體的 Lock
實(shí)現(xiàn)類坊罢,分別在對(duì)應(yīng)的 XxxLockRegitry 實(shí)現(xiàn)類來創(chuàng)建。
RedisLockRegistry 里obtain()方法實(shí)現(xiàn)類為 RedisLock
擅耽,RedisLock內(nèi)部活孩,在Springboot2.x(Spring5)版本中是通過SET + PEXIPRE 命令結(jié)合lua腳本實(shí)現(xiàn)的,在Springboot1.x(Spring4)版本中乖仇,是通過SETNX命令實(shí)現(xiàn)的憾儒。
ZookeeperLockRegistry 里obtain()方法實(shí)現(xiàn)類為 ZkLock
,ZkLock內(nèi)部基于 Apache Curator 框架實(shí)現(xiàn)的乃沙。
JdbcLockRegistry 里obtain()方法實(shí)現(xiàn)類為 JdbcLock
起趾,JdbcLock內(nèi)部基于一張INT_LOCK
數(shù)據(jù)庫鎖表實(shí)現(xiàn)的,通過JdbcTemplate來操作崔涂。
客戶端使用方法:
private final String registryKey = "sb2";
RedisLockRegistry lockRegistry = new RedisLockRegistry(getConnectionFactory(), this.registryKey);
Lock lock = lockRegistry.obtain("foo");
lock.lock();
try {
// doSth...
}
finally {
lock.unlock();
}
}
下面以目前最新版本的實(shí)現(xiàn)阳掐,說明加鎖和解鎖的具體過程。
RedisLockRegistry$RedisLock類lock()加鎖流程:
加鎖步驟:
1)lockKey為registryKey:path冷蚂,本例中為sb2:foo缭保,客戶端C1優(yōu)先申請(qǐng)加鎖。
2)執(zhí)行l(wèi)ua腳本蝙茶,get lockKey不存在艺骂,則set lockKey成功,值為clientid(UUID)隆夯,過期時(shí)間默認(rèn)60秒钳恕。
3)客戶端C1同一個(gè)線程重復(fù)加鎖,pexpire lockKey蹄衷,重置過期時(shí)間為60秒忧额。
4)客戶端C2申請(qǐng)加鎖,執(zhí)行l(wèi)ua腳本愧口,get lockKey已存在睦番,并且跟已加鎖的clientid不同,加鎖失敗
5)客戶端C2掛起耍属,每隔100ms再次嘗試加鎖托嚣。
RedisLock#lock()加鎖源碼實(shí)現(xiàn):
大家可以對(duì)照上面的流程圖配合你理解。
@Override
public void lock() {
this.localLock.lock();
while (true) {
try {
while (!obtainLock()) {
Thread.sleep(100); //NOSONAR
}
break;
}
catch (InterruptedException e) {
/*
* This method must be uninterruptible so catch and ignore
* interrupts and only break out of the while loop when
* we get the lock.
*/
}
catch (Exception e) {
this.localLock.unlock();
rethrowAsLockException(e);
}
}
}
// 基于Spring封裝的RedisTemplate來操作的
private boolean obtainLock() {
Boolean success =
RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
String.valueOf(RedisLockRegistry.this.expireAfter));
boolean result = Boolean.TRUE.equals(success);
if (result) {
this.lockedAt = System.currentTimeMillis();
}
return result;
}
執(zhí)行的lua腳本代碼:
private static final String OBTAIN_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
RedisLockRegistry$RedisLock類unlock()解鎖流程:
RedisLock#unlock()源碼實(shí)現(xiàn):
@Override
public void unlock() {
if (!this.localLock.isHeldByCurrentThread()) {
throw new IllegalStateException("You do not own lock at " + this.lockKey);
}
if (this.localLock.getHoldCount() > 1) {
this.localLock.unlock();
return;
}
try {
if (!isAcquiredInThisProcess()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
if (Thread.currentThread().isInterrupted()) {
RedisLockRegistry.this.executor.execute(this::removeLockKey);
}
else {
removeLockKey();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Released lock; " + this);
}
}
catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
finally {
this.localLock.unlock();
}
}
// 刪除緩存Key
private void removeLockKey() {
if (this.unlinkAvailable) {
try {
RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);
}
catch (Exception ex) {
LOGGER.warn("The UNLINK command has failed (not supported on the Redis server?); " +
"falling back to the regular DELETE command", ex);
this.unlinkAvailable = false;
RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
}
}
else {
RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
}
}
unlock()解鎖方法里發(fā)現(xiàn)厚骗,并不是直接就調(diào)用Redis的DEL命令刪除Key示启,這也是在Springboot2.x版本中做的一個(gè)優(yōu)化,Redis4.0版本以上提供了UNLINK命令领舰。
換句話說夫嗓,最新版本分布式鎖實(shí)現(xiàn)迟螺,要求是Redis4.0以上版本才能使用。
看下Redis官網(wǎng)給出的一段解釋:
This command is very similar to DEL: it removes the specified keys.
Just like DEL a key is ignored if it does not exist. However the
command performs the actual memory reclaiming in a different thread,
so it is not blocking, while DEL is. This is where the command name
comes from: the command just unlinks the keys from the keyspace. The
actual removal will happen later asynchronously.
DEL始終在阻止模式下釋放值部分啤月。但如果該值太大煮仇,如對(duì)于大型LIST或HASH的分配太多,它會(huì)長時(shí)間阻止Redis谎仲,為了解決這個(gè)問題浙垫,Redis實(shí)現(xiàn)了UNLINK命令,即「非阻塞」刪除郑诺。如果值很小夹姥,則DEL一般與UNLINK效率上差不多。
本質(zhì)上辙诞,這種加鎖方式還是使用的SETNX實(shí)現(xiàn)的辙售,而且Spring只是做了一層薄薄的封裝,支持可重入加鎖飞涂,超時(shí)等待旦部,可中斷加鎖。
但是有個(gè)問題较店,鎖的過期時(shí)間不能靈活設(shè)置士八,客戶端初始化時(shí),創(chuàng)建RedisLockRegistry時(shí)允許設(shè)置梁呈,但是是全局的婚度。
/**
* Constructs a lock registry with the supplied lock expiration.
* @param connectionFactory The connection factory.
* @param registryKey The key prefix for locks.
* @param expireAfter The expiration in milliseconds.
*/
public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {
Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
Assert.notNull(registryKey, "'registryKey' cannot be null");
this.redisTemplate = new StringRedisTemplate(connectionFactory);
this.obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
this.registryKey = registryKey;
this.expireAfter = expireAfter;
}
expireAfter參數(shù)是全局的,同樣會(huì)存在問題官卡,可能是鎖過期時(shí)間到了蝗茁,但是業(yè)務(wù)還沒有處理完,這把鎖又被另外的客戶端獲得寻咒,進(jìn)而會(huì)導(dǎo)致一些其他問題哮翘。
經(jīng)過對(duì)源碼的分析,其實(shí)我們也可以借鑒RedisLockRegistry實(shí)現(xiàn)的基礎(chǔ)上毛秘,自行封裝實(shí)現(xiàn)分布式鎖忍坷,比如:
1、允許支持按照不同的Key設(shè)置過期時(shí)間熔脂,而不是全局的?
2柑肴、當(dāng)業(yè)務(wù)沒有處理完成霞揉,當(dāng)前客戶端啟動(dòng)個(gè)定時(shí)任務(wù)探測(cè),自動(dòng)延長過期時(shí)間晰骑?
自己實(shí)現(xiàn)适秩?嫌麻煩绊序?別急別急!業(yè)界已經(jīng)有現(xiàn)成的實(shí)現(xiàn)方案了秽荞,那就是 Redisson
框架骤公,在后文Redisson部分進(jìn)一步分析。
站在Redis集群角度看問題
從Redis主從
架構(gòu)上來考慮扬跋,依然存在問題阶捆。因?yàn)?Redis 集群數(shù)據(jù)同步到各個(gè)節(jié)點(diǎn)時(shí)是異步的,如果在 Master 節(jié)點(diǎn)獲取到鎖后钦听,在沒有同步到其它節(jié)點(diǎn)時(shí)洒试,Master 節(jié)點(diǎn)崩潰了,此時(shí)新的 Master 節(jié)點(diǎn)依然可以獲取鎖朴上,所以多個(gè)應(yīng)用服務(wù)可以同時(shí)獲取到鎖垒棋。
基于以上的考慮,Redis之父Antirez提出了一個(gè)RedLock算法
痪宰。
RedLock算法實(shí)現(xiàn)過程分析:
假設(shè)Redis部署模式是Redis Cluster叼架,總共有5個(gè)master節(jié)點(diǎn),通過以下步驟獲取一把鎖:
1)獲取當(dāng)前時(shí)間戳衣撬,單位是毫秒
2)輪流嘗試在每個(gè)master節(jié)點(diǎn)上創(chuàng)建鎖乖订,過期時(shí)間設(shè)置較短,一般就幾十毫秒淮韭,為了避免遇到故障的節(jié)點(diǎn)阻塞太長時(shí)間垢粮。如果當(dāng)前節(jié)點(diǎn)在幾十毫秒內(nèi)沒有加鎖成功,則放棄該節(jié)點(diǎn)靠粪,直接跳到下一個(gè)節(jié)點(diǎn)加鎖蜡吧。
3)嘗試在大多數(shù)節(jié)點(diǎn)上建立一個(gè)鎖,比如5個(gè)節(jié)點(diǎn)就要求是3個(gè)節(jié)點(diǎn)(n / 2 +1)
4)客戶端計(jì)算建立好鎖的時(shí)間占键,如果建立鎖的時(shí)間小于超時(shí)時(shí)間昔善,就算建立成功了
5)要是鎖建立失敗了,那么就依次刪除這個(gè)鎖
6)只要有客戶端創(chuàng)建成功了分布式鎖畔乙,其他客戶端就得不斷輪詢?nèi)L試獲取鎖
以上過程前文也提到了君仆,進(jìn)一步分析RedLock算法的實(shí)現(xiàn)依然可能存在問題,也是Martain和Antirez兩位大佬爭論的焦點(diǎn)牲距。
客戶端加鎖成功必須滿足一下倆個(gè)的條件:
1.必須是在大多數(shù)節(jié)點(diǎn)上加鎖成功
- 加鎖時(shí)間必須小于鎖的過期時(shí)間
問題1:節(jié)點(diǎn)崩潰重啟
節(jié)點(diǎn)崩潰重啟返咱,會(huì)出現(xiàn)多個(gè)客戶端持有鎖。
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A牍鞠、B咖摹、 C、 D难述、 E萤晴。設(shè)想發(fā)生了如下的事件序列:
1)客戶端C1成功對(duì)Redis集群中A吐句、B、C三個(gè)節(jié)點(diǎn)加鎖成功(但D和E沒有鎖椎甓痢)嗦枢。
2)節(jié)點(diǎn)C Duang的一下,崩潰重啟了屯断,但客戶端C1在節(jié)點(diǎn)C加鎖未持久化完文虏,丟了。
3)節(jié)點(diǎn)C重啟后裹纳,由于現(xiàn)在只有A,B倆節(jié)點(diǎn)加鎖择葡,沒有滿足必須加鎖節(jié)點(diǎn)為n/2+1,所以屬于其他客戶端可以繼續(xù)加鎖√暄酰客戶端C2成功對(duì)Redis集群中C敏储、D、 E嘗試加鎖成功了朋鞍。
這樣已添,悲劇了吧!客戶端C1和C2同時(shí)獲得了同一把分布式鎖滥酥。我們提出redLock方案就是為了解決redis在集群中分布式鎖中重復(fù)加鎖的問題的更舞,現(xiàn)在redLock同樣出現(xiàn)了這個(gè)問題,接著繼續(xù)往下看坎吻。
為了應(yīng)對(duì)節(jié)點(diǎn)重啟引發(fā)的鎖失效問題缆蝉,Antirez提出了延遲重啟
的概念,即一個(gè)節(jié)點(diǎn)崩潰后瘦真,先不立即重啟它刊头,而是等待一段時(shí)間再重啟,等待的時(shí)間大于鎖的有效時(shí)間诸尽。
采用這種方式原杂,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會(huì)過期,它在重啟后就不會(huì)對(duì)現(xiàn)有的鎖造成影響您机。
這其實(shí)也是通過人為補(bǔ)償措施穿肄,降低不一致發(fā)生的概率。
問題2:時(shí)鐘跳躍
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A际看、B咸产、 C、 D仲闽、 E脑溢。設(shè)想發(fā)生了如下的事件序列:
1)客戶端C1成功對(duì)Redis集群中A、B蔼囊、 C三個(gè)節(jié)點(diǎn)成功加鎖焚志。但因網(wǎng)絡(luò)問題,與D和E通信失敗畏鼓。
2)節(jié)點(diǎn)C上的時(shí)鐘發(fā)生了向前跳躍酱酬,導(dǎo)致它上面維護(hù)的鎖快速過期。
3)客戶端C2對(duì)Redis集群中節(jié)點(diǎn)C云矫、 D膳沽、 E成功加了同一把鎖。
此時(shí)让禀,又悲劇了吧挑社!客戶端C1和C2同時(shí)都持有著同一把分布式鎖。
為了應(yīng)對(duì)時(shí)鐘跳躍
引發(fā)的鎖失效問題巡揍,Antirez提出了應(yīng)該禁止人為修改系統(tǒng)時(shí)間痛阻,使用一個(gè)不會(huì)進(jìn)行「跳躍式」調(diào)整系統(tǒng)時(shí)鐘的ntpd程序。這也是通過人為補(bǔ)償措施腮敌,降低不一致發(fā)生的概率阱当。
如果想要使用redLock的話,
延遲重啟
是必須需要保證的糜工,否則redLock方案毫無意義弊添。
其次,避免時(shí)鐘跳躍的問題捌木,必須禁止用戶修改系統(tǒng)時(shí)間油坝,如果倆節(jié)點(diǎn)一個(gè)在北極一個(gè)在南極,那么時(shí)間差的問題仍然會(huì)存在的刨裆,也就是如果倆節(jié)點(diǎn)距離太原澈圈,仍然有時(shí)鐘跳躍的問題。
存在這么大爭議的算法實(shí)現(xiàn)崔拥,還是不推薦使用的极舔。
一般情況下,本文鎖介紹的框架提供的分布式鎖實(shí)現(xiàn)已經(jīng)能滿足大部分需求了链瓦。
小結(jié):
上述拆魏,我們對(duì)spring-integration-redis實(shí)現(xiàn)原理進(jìn)行了深入分析,還對(duì)RedLock存在爭議的問題做了分析慈俯。
除此以外渤刃,我們還提到了spring-integration中集成了 Jdbc、Zookeeper贴膘、Gemfire實(shí)現(xiàn)的分布式鎖卖子,Gemfire和Jdbc大家感興趣可以自行去看下。
基于Redisson實(shí)現(xiàn)分布式鎖
Redisson 是 Redis 的 Java 實(shí)現(xiàn)的客戶端刑峡,其 API 提供了比較全面的 Redis 命令的支持洋闽。
Jedis 簡單使用阻塞的 I/O 和 Redis 交互玄柠,Redission 通過 Netty 支持非阻塞 I/O。
Redisson 封裝了鎖的實(shí)現(xiàn)诫舅,讓我們像操作我們的本地 Lock一樣來使用羽利,除此之外還有對(duì)集合、對(duì)象刊懈、常用緩存框架等做了友好的封裝这弧,易于使用。
截止目前虚汛,Github上 Star 數(shù)量為 11.8k匾浪,說明該開源項(xiàng)目值得關(guān)注和使用。
Redisson分布式鎖Github:
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
Redisson 可以便捷的支持多種Redis部署架構(gòu):
Redis 單機(jī)
Master-Slave + Sentinel 哨兵
Redis-Cluster集群
使用上非常簡單卷哩,RedissonClient客戶端提供了眾多的接口實(shí)現(xiàn)蛋辈,支持可重入鎖、公平鎖殉疼、讀寫鎖梯浪、鎖超時(shí)、RedLock等都提供了完整實(shí)現(xiàn)瓢娜。
lock()加鎖流程:
為了兼容老的版本挂洛,Redisson里都是通過lua腳本執(zhí)行Redis命令的,同時(shí)保證了原子性操作眠砾。
加鎖執(zhí)行的lua腳本:
Redis里的Hash散列結(jié)構(gòu)存儲(chǔ)的虏劲。
參數(shù)解釋:
KEY[1]:要加鎖的Key名稱,比如示例中的myLock褒颈。
ARGV[1]:針對(duì)加鎖的Key設(shè)置的過期時(shí)間
ARGV[2]:Hash結(jié)構(gòu)中Key名稱柒巫,lockName為UUID:線程ID
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
1)客戶端C1申請(qǐng)加鎖,key為myLock谷丸。
2)如果key不存在堡掏,通過hset設(shè)置值,通過pexpire設(shè)置過期時(shí)間刨疼。同時(shí)開啟Watchdog任務(wù)泉唁,默認(rèn)每隔10秒中判斷一下,如果key還在揩慕,重置過期時(shí)間到30秒亭畜。
開啟WatchDog源碼:
3)客戶端C1相同線程再次加鎖,如果key存在迎卤,判斷Redis里Hash中的lockName跟當(dāng)前線程lockName相同拴鸵,則將Hash中的lockName的值加1,代表支持可重入加鎖。
4)客戶單C2申請(qǐng)加鎖劲藐,如果key存在八堡,判斷Redis里Hash中的lockName跟當(dāng)前線程lockName不同,則執(zhí)行pttl返回剩余過期時(shí)間聘芜。
5)客戶端C2線程內(nèi)不斷嘗試pttl時(shí)間秕重,此處是基于Semaphore信號(hào)量實(shí)現(xiàn)的,有許可立即返回厉膀,否則等到pttl時(shí)間還是沒有得到許可,繼續(xù)重試二拐。
重試源碼:
Redisson這樣的實(shí)現(xiàn)就解決了服鹅,當(dāng)業(yè)務(wù)處理時(shí)間比過期時(shí)間長的問題。
同時(shí)百新,Redisson 還自己擴(kuò)展 Lock 接口企软,叫做 RLock 接口,擴(kuò)展了鎖接口饭望,比如給 Key 設(shè)定過期時(shí)間仗哨,非阻塞+超時(shí)時(shí)間等。
void lock(long leaseTime, TimeUnit unit);
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
redisson里的WatchDog(看門狗)邏輯保證了沒有死鎖發(fā)生铅辞。
如果客戶端宕機(jī)了厌漂,WatchDog任務(wù)也就跟著停掉了。此時(shí)斟珊,不會(huì)對(duì)Key重置過期時(shí)間了苇倡,等掛掉的客戶端持有的Key過期時(shí)間到了,鎖自動(dòng)釋放囤踩,其他客戶端嘗試獲得這把鎖旨椒。
可以進(jìn)一步看官網(wǎng)的關(guān)于WatchDog描述:
If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be changed through Config.lockWatchdogTimeout setting.
unlock()解鎖過程也是同樣的,通過lua腳本執(zhí)行一大坨指令的堵漱。
解鎖lua腳本:
根據(jù)剛剛對(duì)加鎖過程的分析综慎,大家可以自行看下腳本分析下。
基于Zookeeper實(shí)現(xiàn)分布式鎖
Zookeeper 是一種提供「分布式服務(wù)協(xié)調(diào)」的中心化服務(wù)勤庐,是以 Paxos 算法為基礎(chǔ)實(shí)現(xiàn)的示惊。Zookeeper數(shù)據(jù)節(jié)點(diǎn)和文件目錄類似,同時(shí)具有Watch機(jī)制埃元,基于這兩個(gè)特性涝涤,得以實(shí)現(xiàn)分布式鎖功能。
數(shù)據(jù)節(jié)點(diǎn):
順序臨時(shí)節(jié)點(diǎn):Zookeeper 提供一個(gè)多層級(jí)的節(jié)點(diǎn)命名空間(節(jié)點(diǎn)稱為 Znode)岛杀,每個(gè)節(jié)點(diǎn)都用一個(gè)以斜杠(/)分隔的路徑來表示阔拳,而且每個(gè)節(jié)點(diǎn)都有父節(jié)點(diǎn)(根節(jié)點(diǎn)除外),非常類似于文件系統(tǒng)。
節(jié)點(diǎn)類型可以分為持久節(jié)點(diǎn)(PERSISTENT )糊肠、臨時(shí)節(jié)點(diǎn)(EPHEMERAL)辨宠,每個(gè)節(jié)點(diǎn)還能被標(biāo)記為有序性(SEQUENTIAL),一旦節(jié)點(diǎn)被標(biāo)記為有序性货裹,那么整個(gè)節(jié)點(diǎn)就具有順序自增的特點(diǎn)嗤形。
一般我們可以組合這幾類節(jié)點(diǎn)來創(chuàng)建我們所需要的節(jié)點(diǎn),例如弧圆,創(chuàng)建一個(gè)持久節(jié)點(diǎn)作為父節(jié)點(diǎn)赋兵,在父節(jié)點(diǎn)下面創(chuàng)建臨時(shí)節(jié)點(diǎn),并標(biāo)記該臨時(shí)節(jié)點(diǎn)為有序性搔预。
Watch 機(jī)制:
Zookeeper 還提供了另外一個(gè)重要的特性霹期,Watcher(事件監(jiān)聽器)。
ZooKeeper 允許用戶在指定節(jié)點(diǎn)上注冊(cè)一些 Watcher拯田,并且在一些特定事件觸發(fā)的時(shí)候历造,ZooKeeper 服務(wù)端會(huì)將事件通知給用戶。
圖解Zookeeper實(shí)現(xiàn)分布式鎖:
首先船庇,我們需要建立一個(gè)父節(jié)點(diǎn)吭产,節(jié)點(diǎn)類型為持久節(jié)點(diǎn)(PERSISTENT)如圖中的 /locks/lock_name1
節(jié)點(diǎn) ,每當(dāng)需要訪問共享資源時(shí)鸭轮,就會(huì)在父節(jié)點(diǎn)下建立相應(yīng)的順序子節(jié)點(diǎn)臣淤,節(jié)點(diǎn)類型為臨時(shí)節(jié)點(diǎn)(EPHEMERAL),且標(biāo)記為有序性(SEQUENTIAL)窃爷,并且以臨時(shí)節(jié)點(diǎn)名稱 + 父節(jié)點(diǎn)名稱 + 順序號(hào)組成特定的名字荒典,如圖中的 /0000000001 /0000000002 /0000000003
作為臨時(shí)有序節(jié)點(diǎn)。
在建立子節(jié)點(diǎn)后吞鸭,對(duì)父節(jié)點(diǎn)下面的所有以臨時(shí)節(jié)點(diǎn)名稱 name 開頭的子節(jié)點(diǎn)進(jìn)行排序寺董,判斷剛剛建立的子節(jié)點(diǎn)順序號(hào)是否是最小的節(jié)點(diǎn),如果是最小節(jié)點(diǎn)刻剥,則獲得鎖遮咖。
如果不是最小節(jié)點(diǎn),則阻塞等待鎖造虏,并且獲得該節(jié)點(diǎn)的上一順序節(jié)點(diǎn)御吞,為其注冊(cè)監(jiān)聽事件,等待節(jié)點(diǎn)對(duì)應(yīng)的操作獲得鎖漓藕。當(dāng)調(diào)用完共享資源后陶珠,刪除該節(jié)點(diǎn),關(guān)閉 zk享钞,進(jìn)而可以觸發(fā)監(jiān)聽事件揍诽,釋放該鎖。
// 加鎖
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) )
{
try
{
// do some work inside of the critical section here
}
finally
{
lock.release();
}
}
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
// ... 其他代碼略
InterProcessMutex 是 Curator 實(shí)現(xiàn)的可重入鎖,可重入鎖源碼過程分析:
加鎖流程:
1)可重入鎖記錄在 ConcurrentMap threadData 這個(gè) Map 里面暑脆。</thread,>
2)如果 threadData.get(currentThread) 是有值的那么就證明是可重入鎖渠啤,然后記錄就會(huì)加 1。
3)資源目錄下創(chuàng)建一個(gè)節(jié)點(diǎn):比如這里創(chuàng)建一個(gè) /0000000002 這個(gè)節(jié)點(diǎn)添吗,這個(gè)節(jié)點(diǎn)需要設(shè)置為 EPHEMERAL_SEQUENTIAL 也就是臨時(shí)節(jié)點(diǎn)并且有序沥曹。
4)獲取當(dāng)前目錄下所有子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)是否是最小的節(jié)點(diǎn)碟联。
5)如果是最小的節(jié)點(diǎn)妓美,則獲取到鎖。如果不是最小的節(jié)點(diǎn)鲤孵,則證明前面已經(jīng)有人獲取到鎖了部脚,那么需要獲取自己節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)。
6)節(jié)點(diǎn) /0000000002 的前一個(gè)節(jié)點(diǎn)是 /0000000001裤纹,我們獲取到這個(gè)節(jié)點(diǎn)之后,再上面注冊(cè) Watcher丧没,Watcher 調(diào)用的是 object.notifyAll()鹰椒,用來解除阻塞。
7)object.wait(timeout) 或 object.wait() 進(jìn)行阻塞等待
解鎖流程:
1)如果可重入鎖次數(shù)減1后呕童,加鎖次數(shù)不為 0 直接返回漆际,減1后加鎖次數(shù)為0,繼續(xù)夺饲。
2)刪除當(dāng)前節(jié)點(diǎn)奸汇。
3)刪除 threadDataMap 里面的可重入鎖的數(shù)據(jù)。
4最后的總結(jié)
上面介紹的諸如Apache Curator往声、Redisson擂找、Spring框架集成的分布式鎖,既然是框架實(shí)現(xiàn)浩销,會(huì)考慮用戶需求贯涎,盡量設(shè)計(jì)和實(shí)現(xiàn)通用的分布式鎖接口。
基本都涵蓋了如下的方式實(shí)現(xiàn):
當(dāng)然慢洋,Redisson和Curator都是自己定義的分布式鎖接口實(shí)現(xiàn)的塘雳,易于擴(kuò)展。
Curator里自定義了InterProcessLock接口普筹,Redisson里自定義RLock接口败明,繼承了 java.util.concurrent.locks.Lock接口。
對(duì)于Redis實(shí)現(xiàn)的分布式鎖:
大部分需求下太防,不會(huì)遇到「極端復(fù)雜場景」妻顶,基于Redis實(shí)現(xiàn)分布式鎖很常用,性能也高。
它獲取鎖的方式簡單粗暴盈包,獲取不到鎖直接不斷嘗試獲取鎖沸呐,比較消耗性能。
另外來說的話呢燥,redis的設(shè)計(jì)定位決定了它的數(shù)據(jù)并不是強(qiáng)一致性的崭添,沒有一致性算法,在某些極端情況下叛氨,可能會(huì)出現(xiàn)問題呼渣,鎖的模型不夠健壯。
即便有了Redlock算法的實(shí)現(xiàn)寞埠,但存在爭議屁置,某些復(fù)雜場景下,也無法保證其實(shí)現(xiàn)完全沒有問題仁连,并且也是比較消耗性能的蓝角。
對(duì)于Zookeeper實(shí)現(xiàn)的分布式鎖:
Zookeeper優(yōu)點(diǎn):
天生設(shè)計(jì)定位是分布式協(xié)調(diào),強(qiáng)一致性饭冬。鎖的模型健壯使鹅、簡單易用、適合做分布式鎖昌抠。
如果獲取不到鎖患朱,只需要添加一個(gè)監(jiān)聽器就可以了,不用一直輪詢炊苫,性能消耗較小裁厅。
如果客戶端宕機(jī),也沒關(guān)系侨艾,臨時(shí)節(jié)點(diǎn)會(huì)自動(dòng)刪除执虹,觸發(fā)監(jiān)聽器通知下一個(gè)節(jié)點(diǎn)。
Zookeeper缺點(diǎn):
若有大量的客戶端頻繁的申請(qǐng)加鎖唠梨、釋放鎖声畏,對(duì)于ZK集群的壓力會(huì)比較大。
另外姻成,本文對(duì)Spring-integration集成Redis實(shí)現(xiàn)的分布式鎖做了詳細(xì)剖析插龄,可以直接使用,更推薦直接使用 Redisson
科展,實(shí)現(xiàn)了非常多的分布式鎖各種機(jī)制均牢,有單獨(dú)開放Springboot集成的jar包,使用上也是非常方便的才睹。
文章開頭部分提到的幾個(gè)業(yè)務(wù)場景徘跪,經(jīng)過對(duì)分布式鎖家族的介紹和原理分析甘邀,可以自行選擇技術(shù)方案了。
以上垮庐,一定有一款能滿足你的需求松邪,希望大家有所收獲!
碼字不易哨查,文章不妥之處逗抑,歡迎留言斧正。