什么是鎖步清?
在單進程的系統(tǒng)中,當(dāng)存在多個線程可以同時改變某個變量(可變共享變量)時飘哨,就需要對變量或代碼塊做同步胚想,使其在修改這種變量時能夠線性執(zhí)行(按順序執(zhí)行)。
而同步的本質(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)記。
什么是分布式鎖窿侈?
通常我們會把同一個服務(wù)分布式部署到多個服務(wù)器節(jié)點上炼幔,從而提高服務(wù)的可用性,并發(fā)量史简。
- 分布式與單機情況下最大的不同在于其不是多線程而是多進程乃秀。
- 多線程由于可以共享堆內(nèi)存,因此可以簡單的采取內(nèi)存作為標(biāo)記存儲圆兵。而進程之間甚至可能都不在同一臺物理機上跺讯,因此需要將標(biāo)記存儲在一個所有進程都能看到的地方。
當(dāng)在分布式模型下殉农,數(shù)據(jù)只有一份刀脏,此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進程數(shù)。與單機模式下的鎖不僅需要保證進程可見超凳,還需要考慮進程與鎖之間的網(wǎng)絡(luò)問題火本。
分布式鎖的一些特點
當(dāng)我們確定了在不同節(jié)點上需要分布式鎖,那么我們需要了解分布式鎖到底應(yīng)該有哪些特點:
- 互斥性:和我們本地鎖一樣互斥性是最基本聪建,但是分布式鎖需要保證在不同節(jié)點的不同線程的互斥。
- 可重入性:同一個節(jié)點上的同一個線程如果獲取了鎖之后那么也可以再次獲取這個鎖茫陆。
- 鎖超時:和本地鎖一樣支持鎖超時金麸,防止死鎖。
- 高效簿盅,高可用:加鎖和解鎖需要高效挥下,同時也需要保證高可用防止分布式鎖失效揍魂,可以增加降級。
- 支持阻塞和非阻塞:和ReentrantLock一樣支持lock和trylock以及tryLock(long timeOut)棚瘟。
- 支持公平鎖和非公平鎖(可選):公平鎖的意思是按照請求加鎖的順序獲得鎖现斋,非公平鎖就相反是無序的。這個一般來說實現(xiàn)的比較少偎蘸。
常見的分布式鎖實現(xiàn)方式
我們了解了一些特點之后庄蹋,我們一般實現(xiàn)分布式鎖有以下幾個方式:
- MySql
- Redis
- Zookeeper
- 自研分布式鎖(如谷歌的Chubby)
一:基于MySQL數(shù)據(jù)庫做分布式鎖
鎖思想
首先來說一下Mysql分布式鎖的實現(xiàn)原理,相對來說這個比較容易理解迷雪,畢竟數(shù)據(jù)庫和我們開發(fā)人員在平時的開發(fā)中息息相關(guān)限书。對于分布式鎖我們一般會提供三個方法:
- lock()
- tryLock()和tryLock(long timeout)
- unlock()
- lock()
lock一般是阻塞式的獲取鎖,意思就是不獲取到鎖誓不罷休章咧,那么我們可以寫一個死循環(huán)來執(zhí)行其操作:
mysqlLock.lcok內(nèi)部是一個sql,為了達到可重入鎖的效果那么我們應(yīng)該先進行查詢倦西,如果有值,那么需要比較node_info是否一致赁严,這里的node_info可以用機器IP和線程名字來表示扰柠,如果一致那么就加可重入鎖count的值,如果不一致那么就返回false疼约。如果沒有值那么直接插入一條數(shù)據(jù)卤档。
需要注意的是這一段代碼需要加事務(wù),必須要保證這一系列操作的原子性忆谓。
- tryLock()和tryLock(long timeout)
tryLock()是非阻塞獲取鎖裆装,如果獲取不到那么就會馬上返回,代碼可以如下:
tryLock(long timeout)實現(xiàn)如下:
mysqlLock.lock和上面一樣倡缠,但是要注意的是select ... for update這個是阻塞的獲取行鎖哨免,如果同一個資源并發(fā)量較大還是有可能會退化成阻塞的獲取鎖。
- unlock()
unlock的話如果這里的count為1那么可以刪除昙沦,如果大于1那么需要減去1琢唾。
鎖超時問題
我們有可能會遇到我們的機器節(jié)點掛了,那么這個鎖就不會得到釋放盾饮,我們可以啟動一個定時任務(wù)采桃,通過計算一般我們處理任務(wù)的一般的時間,比如是5ms丘损,那么我們可以稍微擴大一點普办,當(dāng)這個鎖超過20ms沒有被釋放我們就可以認(rèn)定是節(jié)點掛了然后將其直接釋放。
Mysql小結(jié)
- 適用場景: Mysql分布式鎖一般適用于資源不存在數(shù)據(jù)庫徘钥,如果數(shù)據(jù)庫存在比如訂單衔蹲,那么可以直接對這條數(shù)據(jù)加行鎖,不需要我們上面多的繁瑣的步驟呈础,比如一個訂單舆驶,那么我們可以用select * from order_table where id = 'xxx' for update進行加行鎖橱健,那么其他的事務(wù)就不能對其進行修改。
- 優(yōu)點:理解起來簡單沙廉,不需要維護額外的第三方中間件(比如Redis,Zk)拘荡。
- 缺點:雖然容易理解但是實現(xiàn)起來較為繁瑣,需要自己考慮鎖超時撬陵,加事務(wù)等等珊皿。性能局限于數(shù)據(jù)庫,一般對比緩存來說性能較低袱结。對于高并發(fā)的場景并不是很適合亮隙。
- Mysql事務(wù)是線程安全的
- Mysql事務(wù)并發(fā)問題可以通過設(shè)置事務(wù)的隔離級別來解決
方法一:基于表主鍵唯一做分布式鎖
基于樂觀鎖
利用主鍵唯一的特性,如果有多個請求同時提交到數(shù)據(jù)庫的話垢夹,數(shù)據(jù)庫會保證只有一個操作可以成功溢吻,那么我們就可以認(rèn)為操作成功的那個線程獲得了該方法的鎖,當(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)主鍵進行防重程梦。
方法二:基于數(shù)據(jù)庫排他鎖做分布式鎖
基于悲觀鎖
在查詢語句后面增加for update,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖 (注意: InnoDB 引擎在加鎖的時候橘荠,只有通過索引進行檢索的時候才會使用行級鎖屿附,否則會使用表級鎖。這里我們希望使用行級鎖砾医,就要給要執(zhí)行的方法字段名添加索引拿撩,值得注意的是,這個索引一定要創(chuàng)建成唯一索引如蚜,否則會出現(xiàn)多個重載方法之間無法同時被訪問的問題压恒。重載方法的話建議把參數(shù)類型也加上。)错邦。當(dāng)某條記錄被加上排他鎖之后探赫,其他線程無法再在該行記錄上增加排他鎖。
我們可以認(rèn)為獲得排他鎖的線程即可獲得分布式鎖撬呢,當(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 認(rèn)為全表掃效率更高恢共,比如對一些很小的表战秋,它就不會使用索引,這種情況下 InnoDB 將使用表鎖讨韭,而不是行鎖脂信。如果發(fā)生這種情況就悲劇了。
還有一個問題透硝,就是我們要使用排他鎖來進行分布式鎖的 lock狰闪,那么一個排他鎖長時間不提交,就會占用數(shù)據(jù)庫連接濒生。一旦類似的連接變得多了埋泵,就可能把數(shù)據(jù)庫連接池撐爆。
優(yōu)缺點
- 優(yōu)點:簡單罪治,易于理解
- 缺點:會有各種各樣的問題(操作數(shù)據(jù)庫需要一定的開銷丽声,使用數(shù)據(jù)庫的行級鎖并不一定靠譜,性能不靠譜)
基于 Redis 做分布式鎖
方案一:
基于 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è)置。
使用步驟
1何暮、setnx(lockkey, 1) 如果返回 0,則說明占位失旑硌辍海洼;如果返回 1,則說明占位成功
2富腊、expire() 命令對 lockkey 設(shè)置超時時間坏逢,為的是避免死鎖問題。
3赘被、執(zhí)行完業(yè)務(wù)代碼后是整,可以通過 delete 命令刪除 key。
這個方案其實是可以解決日常工作中的需求的民假,但從技術(shù)方案的探討上來說浮入,可能還有一些可以完善的地方。比如羊异,如果在第一步 setnx 執(zhí)行成功后事秀,在 expire() 命令執(zhí)行成功前,發(fā)生了宕機的現(xiàn)象野舶,那么就依然會出現(xiàn)死鎖的問題易迹。
方案二:
基于 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)時間酥筝,則認(rèn)為這個鎖已經(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è)置的超時時間,則不需要再對鎖進行處理饭宾。
方案三:SET 多參數(shù)加鎖
SET key value [NX|XX] [EX seconds] [PX milliseconds]
解決問題:
1.死鎖問題:需要給key設(shè)置超時時間批糟,一個接口基本是200ms,最大允許500ms看铆,鎖超時時間設(shè)置為1s徽鼎。
2.鎖續(xù)命問題:定時器判斷,獲取鎖內(nèi)容看是不是當(dāng)前鎖set的內(nèi)容弹惦,如果是否淤,則對鎖進行續(xù)命(watch dog),最多續(xù)命一次棠隐。(參考Redisson:這里面初始化了一個定時器石抡,dely 的時間是 internalLockLeaseTime/3。在 Redisson 中助泽,internalLockLeaseTime 是 30s啰扛,也就是每隔 10s 續(xù)期一次嚎京,每次 30s。)
3.釋放鎖:釋放鎖的時候隐解,獲取鎖內(nèi)容看是不是當(dāng)前鎖set的內(nèi)容鞍帝,避免釋放了不是該線程加的鎖。同時停止計時器煞茫。
4.如果鎖獲取失敗
阻塞鎖:則睡眠500ms再次重試帕涌,重試三次,則返回獲取鎖失敗续徽。
非阻塞鎖:直接返回獲取鎖失敗蚓曼。
缺點:
存在一個問題redis master宕機,但是slave節(jié)點沒有同步到鎖
改進點:
鎖超時應(yīng)該是在在客戶端啟動一個定時器來判斷钦扭。定時器線程判斷工作線程是否還在工作辟躏,如果工作中就鎖續(xù)命,假如工作的線程掛了土全,定時器也要判斷,然后釋放鎖会涎,停止定時器裹匙。
方案四:基于 Redlock算法 做分布式鎖
我們想象一個這樣的場景當(dāng)機器A申請到一把鎖之后,如果Redis主宕機了末秃,這個時候從機并沒有同步到這一把鎖概页,那么機器B再次申請的時候就會再次申請到這把鎖,為了解決這個問題Redis作者提出了RedLock紅鎖的算法,在Redisson中也對RedLock進行了實現(xiàn)练慕。
Redlock 是 Redis 的作者 antirez 給出的集群模式的 Redis 分布式鎖惰匙,它基于 N 個完全獨立的 Redis 節(jié)點(通常情況下 N 可以設(shè)置成 5)。
算法的步驟如下:
1铃将、客戶端獲取當(dāng)前時間项鬼,以毫秒為單位。
2劲阎、客戶端嘗試獲取 N 個節(jié)點的鎖绘盟,(每個節(jié)點獲取鎖的方式和前面說的緩存鎖一樣),N 個節(jié)點以相同的 key 和 value 獲取鎖悯仙×湔保客戶端需要設(shè)置接口訪問超時,接口超時時間需要遠遠小于鎖超時時間锡垄,比如鎖自動釋放的時間是 10s沦零,那么接口超時大概設(shè)置 5-50ms。這樣可以在有 redis 節(jié)點宕機后货岭,訪問該節(jié)點時能盡快超時路操,而減小鎖的正常使用疾渴。
3、客戶端計算在獲得鎖的時候花費了多少時間寻拂,方法是用當(dāng)前時間減去在步驟一獲取的時間程奠,只有客戶端獲得了超過 3 個節(jié)點的鎖,而且獲取鎖的時間小于鎖的超時時間祭钉,客戶端才獲得了分布式鎖瞄沙。
4、客戶端獲取的鎖的時間為設(shè)置的鎖超時時間減去步驟三計算出的獲取鎖花費時間慌核。
5距境、如果客戶端獲取鎖失敗了,客戶端會依次刪除所有的鎖垮卓。 使用 Redlock 算法垫桂,可以保證在掛掉最多 2 個節(jié)點的時候,分布式鎖服務(wù)仍然能工作粟按,這相比之前的數(shù)據(jù)庫鎖和緩存鎖大大提高了可用性诬滩,由于 redis 的高效性能,分布式緩存鎖性能并不比數(shù)據(jù)庫鎖差灭将。
通過上面的代碼疼鸟,我們需要實現(xiàn)多個Redis集群,然后進行紅鎖的加鎖庙曙,解鎖空镜。具體的步驟如下:
- 首先生成多個Redis集群的Rlock,并將其構(gòu)造成RedLock捌朴。
- 依次循環(huán)對三個集群進行加鎖吴攒。
- 如果循環(huán)加鎖的過程中加鎖失敗,那么需要判斷加鎖失敗的次數(shù)是否超出了最大值砂蔽,這里的最大值是根據(jù)集群的個數(shù)洼怔,比如三個那么只允許失敗一個,五個的話只允許失敗兩個察皇,要保證多數(shù)成功茴厉。
- 加鎖的過程中需要判斷是否加鎖超時,有可能我們設(shè)置加鎖只能用3ms什荣,第一個集群加鎖已經(jīng)消耗了3ms了矾缓。那么也算加鎖失敗。
- 3稻爬,4步里面加鎖失敗的話嗜闻,那么就會進行解鎖操作,解鎖會對所有的集群在請求一次解鎖桅锄。
可以看見RedLock基本原理是利用多個Redis集群琉雳,用多數(shù)的集群加鎖成功样眠,減少Redis某個集群出故障,造成分布式鎖出現(xiàn)問題的概率翠肘。
優(yōu)點:
性能高
缺點:
失效時間設(shè)置多長時間為好檐束?如何設(shè)置的失效時間太短,方法沒等執(zhí)行完束倍,鎖就自動釋放了被丧,那么就會產(chǎn)生并發(fā)問題。如果設(shè)置的時間太長绪妹,其他獲取鎖的線程就可能要平白的多等一段時間甥桂。
基于 Redisson(Redis的客戶端) 做分布式鎖
Java開發(fā)者都知道Jedis,Jedis是Redis的Java實現(xiàn)的客戶端邮旷,其API提供了比較全面的Redis命令的支持黄选。Redission也是Redis的客戶端,相比于Jedis功能簡單婶肩。Jedis簡單使用阻塞的I/O和redis交互办陷,Redission通過Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了沒有更新律歼,而Redission最新版本是2018.10月更新懂诗。
Redission封裝了鎖的實現(xiàn),其繼承了java.util.concurrent.locks.Lock的接口苗膝,讓我們像操作我們的本地Lock一樣去操作Redission的Lock,下面介紹一下其如何實現(xiàn)分布式鎖植旧。
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é)點。
- 當(dāng)取鎖失敗严蓖,設(shè)置 watch 后則等待 watch 事件到來后薄嫡,再次判斷是否序號最小。
- 取鎖成功則執(zhí)行代碼颗胡,最后釋放鎖(刪除該節(jié)點)毫深。
優(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的原理有所了解。
Zookeeper和Redis做分布式鎖的區(qū)別巡球?
Reids:
- Redis只保證最終一致性言沐,副本間的數(shù)據(jù)復(fù)制是異步進行(Set是寫,Get是讀酣栈,Reids集群
一般是讀寫分離架
構(gòu)险胰,存在主從同步延遲情況),主從切換之后可能有部分?jǐn)?shù)據(jù)沒有復(fù)制過去可能會「丟失鎖」情況矿筝,故強-
致性要求的業(yè)務(wù)不推薦使用Reids起便, 推薦使用zk。 - Redis集群各方法的響應(yīng)時間均為最低窖维。隨著并發(fā)量和業(yè)務(wù)數(shù)量的提升其響應(yīng)時間會有明顯上升(公網(wǎng)集群影
響因素偏大)榆综,但是極限qps可以達到最大且基本無異常
ZooKeeper:
- 使用zookeeper集群,鎖原理是使用zookeeper的臨時順序節(jié)點铸史,臨時順序節(jié)點的生命周期在Client與集群的
Session結(jié)束時結(jié)束鼻疮。因此如果某個Client節(jié)點存在網(wǎng)絡(luò)問題,與Zookeeper集群斷開連接琳轿,Session超時同樣
會導(dǎo)致鎖被錯誤的釋放(導(dǎo)致被其他線程錯誤地持有)判沟,因此zookeeper也無法保證完全一致。 - ZK具有較好的穩(wěn)定性崭篡;響應(yīng)時間抖動很小水评,沒有出現(xiàn)異常。但是隨著并發(fā)量和業(yè)務(wù)數(shù)量的提升其響應(yīng)時間和
qps會明顯下隆媚送。
總結(jié):
- zookeeper每次進行鎖操作前都要創(chuàng)建若千節(jié)點,完成后要釋放節(jié)點寇甸,會浪費很多時間;
- 而Redis只是簡單的數(shù)據(jù)操作塘偎,沒有這個問題。
自研分布式鎖(如谷歌的Chubby)
Chubby為了解決分布式系統(tǒng)中的一致性問題拿霉,其中最常見的就是分布式系統(tǒng)的選主需求及一致性的數(shù)據(jù)存儲吟秩。Chubby選擇通過提供粗粒度鎖服務(wù)的方式實現(xiàn),粗粒度(Coarse-grained)鎖服務(wù)相對于細粒度(Fine-grained)鎖服務(wù)绽淘,指的是應(yīng)用加鎖時間比較長的場景涵防,達到幾個小時或者幾天。
本質(zhì)上Chubby是Google設(shè)計的提供粗粒度鎖服務(wù)的文件系統(tǒng)沪铭,存儲大量小文件壮池,每個文件就代表一個鎖偏瓤。
Spring Integration
Spring Integration在基于Spring的應(yīng)用程序中實現(xiàn)輕量級消息傳遞,并支持通過聲明適配器與外部系統(tǒng)集成椰憋。 Spring Integration的主要目標(biāo)是提供一個簡單的模型來構(gòu)建企業(yè)集成解決方案厅克,同時保持關(guān)注點的分離,這對于生成可維護橙依,可測試的代碼至關(guān)重要证舟。我們熟知的
Spring Cloud Stream的底層就是Spring Integration。
Spring Integration提供的全局鎖目前為如下存儲提供了實現(xiàn):
- Gemfire
- JDBC
- Redis
- Zookeeper
總結(jié)
無論你身處一個什么樣的公司窗骑,最開始的工作可能都需要從最簡單的做起女责。不要提阿里和騰訊的業(yè)務(wù)場景 qps 如何大,因為在這樣的大場景中你未必能親自參與項目创译,親自參與項目未必能是核心的設(shè)計者抵知,是核心的設(shè)計者未必能獨自設(shè)計。希望大家能根據(jù)自己公司業(yè)務(wù)場景昔榴,選擇適合自己項目的方案辛藻。