Spring?Boot整合Zookeeper實(shí)現(xiàn)分布式鎖的場景分析
溫馨提示:本篇文章要求掌握zk的數(shù)據(jù)結(jié)構(gòu),以及臨時(shí)序號節(jié)點(diǎn)!
zk實(shí)現(xiàn)分布式鎖完全是依靠zk節(jié)點(diǎn)類型當(dāng)中的臨時(shí)序號節(jié)點(diǎn)來實(shí)現(xiàn)的
一、Java當(dāng)中關(guān)于鎖的概念
1.1.什么是鎖
鎖是用來控制多個(gè)線程訪問共享資源的方式,一般來說,一個(gè)鎖能夠防止多個(gè)線程同時(shí)訪問共享資源。
1.2.鎖的使用場景
以減庫存為例,庫存這時(shí)候就剩1個(gè),那么我們得保證只會有1個(gè)請求真正的完成減1操作,假如代碼邏輯是,先從庫里查庫存,通過if條件判斷,如果有就減,沒有就返回購買失敗。
這時(shí)候并發(fā)訪問減庫存接口,可能這時(shí)候我們代碼當(dāng)中的if判斷已經(jīng)失效了,多個(gè)請求同時(shí)查到還有一個(gè),并且已經(jīng)進(jìn)入了if判斷,沒有加鎖的話,這時(shí)候事情就比較嚴(yán)重了,庫存一下子就成了負(fù)數(shù)。
對于這塊代碼我們不希望同時(shí)有人減庫存,這時(shí)候就需要加鎖來控制,加完鎖之后,就是將并發(fā)請求改成了串行,也就是不管并發(fā)了多少個(gè)請求,我通過加鎖,只讓1個(gè)請求進(jìn)行減庫存,你們?nèi)帗屾i資源吧,誰先搶到了就是誰的。
1.3.什么是分布式鎖
分布式鎖是控制 微服務(wù)集群 之間同步訪問共享資源的一種方式。
1.4.分布式鎖的使用場景
使用分布式鎖前提:微服務(wù)一定是集群,這里的微服務(wù)不是指的zk,而是指的我們的業(yè)務(wù)模塊,在項(xiàng)目當(dāng)中一般的,由于并發(fā)量較高,往往會將業(yè)務(wù)拆分為一個(gè)模塊一個(gè)模塊,例如訂單模塊,庫存模塊,拆模塊其中一個(gè)目的就是為了針對于模塊的并發(fā)性進(jìn)行集群部署,比如訂單模塊用的比較多,我可以搭建多個(gè)訂單模塊,但是盡管他們的模塊是多個(gè),圍繞的數(shù)據(jù)還是共通的(一個(gè)數(shù)據(jù)庫)。
集群情況下,我們在代碼加普通鎖已經(jīng)解決不了問題,假如現(xiàn)在有三個(gè)庫存微服務(wù),設(shè)置了負(fù)載均衡的方式訪問,普通鎖只能控制自己的服務(wù)減庫存不會出現(xiàn)負(fù)數(shù),但是他控制不了其他兩個(gè)服務(wù),數(shù)據(jù)是共通的,所以這時(shí)候只能使用分布式鎖,通過分布式鎖來控制三個(gè)服務(wù)當(dāng)庫存只有一個(gè)商品的時(shí)候,只能有一個(gè)服務(wù)訪問的請求可以減庫存成功。
二、zk實(shí)現(xiàn)分布式鎖
2.1.zk中鎖的種類:
- 讀鎖:創(chuàng)建?個(gè)臨時(shí)序號節(jié)點(diǎn),節(jié)點(diǎn)的名稱會包含READ的字母,表示是讀鎖,?家都可以讀,要想上讀鎖的前提:
之前的鎖沒有寫鎖 - 寫鎖:創(chuàng)建?個(gè)臨時(shí)序號節(jié)點(diǎn),節(jié)點(diǎn)的名稱會包含WRIT的字母,表示是寫鎖,只有得到寫鎖的才能寫。要想上寫鎖的前提是,
之前沒有任何鎖。
讀鎖和寫鎖完全是按照創(chuàng)建的臨時(shí)序號節(jié)點(diǎn)的名稱來區(qū)分的!
- 序號節(jié)點(diǎn):
創(chuàng)建出的節(jié)點(diǎn),根據(jù)先后順序,會在節(jié)點(diǎn)之后帶上?個(gè)數(shù)值,越往后執(zhí)?數(shù)值越?,類似于mysql的主鍵自增 - 臨時(shí)節(jié)點(diǎn) :臨時(shí)節(jié)點(diǎn)是在會話結(jié)束后,?動(dòng)被刪除的,通過這個(gè)特性,zk可以實(shí)現(xiàn)服務(wù)注冊與發(fā)現(xiàn)的效果。假如會話關(guān)掉后大概10s左右,創(chuàng)建的臨時(shí)節(jié)點(diǎn)就會消失。這個(gè)會話就是指的連接zk的客戶端。
- 臨時(shí)序號節(jié)點(diǎn):就是上面兩個(gè)的結(jié)合體
當(dāng)需要上鎖的時(shí)候,就進(jìn)行創(chuàng)建臨時(shí)序號節(jié)點(diǎn),釋放鎖的時(shí)候就刪除節(jié)點(diǎn)。
2.2.zk如何上讀鎖
- 創(chuàng)建一個(gè)臨時(shí)序號節(jié)點(diǎn)
- 獲取當(dāng)前zk中序號比自己小的所有節(jié)點(diǎn)
- 判斷最小節(jié)點(diǎn)是否是讀鎖:
- 如果不是讀鎖的話,則上鎖失敗,為最小節(jié)點(diǎn)設(shè)置監(jiān)聽。阻塞等待,zk的watch機(jī)制會當(dāng)最小節(jié)點(diǎn)發(fā)生變化時(shí)通知當(dāng)前節(jié)點(diǎn),于是再執(zhí)行第二步的流程
- 如果是讀鎖的話,則上鎖成功
想要上讀鎖,主要就是需要看比他小的節(jié)點(diǎn)當(dāng)中是否有寫鎖。如果有寫鎖,就需要等他用完之后刪除節(jié)點(diǎn),通過watch機(jī)制來通知他,寫鎖已經(jīng)釋放,然后他再進(jìn)行第二步判斷。

2.3.zk如何上寫鎖
- 創(chuàng)建一個(gè)臨時(shí)序號節(jié)點(diǎn)
- 獲取zk中所有的子節(jié)點(diǎn)
- 判斷自己是否是最小的節(jié)點(diǎn):
- 如果是,則上寫鎖成功
- 如果不是,說明前面還有鎖,則上鎖失敗,監(jiān)聽最小的節(jié)點(diǎn),如果最小節(jié)點(diǎn)有變化,則回到第二步。

2.4.?群效應(yīng)
如果?上述的上鎖?式,只要有節(jié)點(diǎn)發(fā)?變化,就會觸發(fā)其他節(jié)點(diǎn)的監(jiān)聽事件,這樣的話對zk的壓??常?,——?群效應(yīng)??梢哉{(diào)整成鏈?zhǔn)奖O(jiān)聽。解決這個(gè)問題。

假如并發(fā)了100個(gè)請求都需要獲取寫鎖,這時(shí)候創(chuàng)建了100個(gè)節(jié)點(diǎn)來監(jiān)聽最小節(jié)點(diǎn),當(dāng)最小節(jié)點(diǎn)發(fā)生變化的時(shí)候,意味著他一下子要進(jìn)行通知100個(gè)節(jié)點(diǎn),zk瞬間會壓力非常大。
所以這時(shí)候可以采用鏈?zhǔn)奖O(jiān)聽,鏈?zhǔn)奖O(jiān)聽仍然是依靠的序號節(jié)點(diǎn)的特點(diǎn)。就好比mysql設(shè)置自增后,不管多少并發(fā)請求,他仍然能保證id的唯一性,zk的序號節(jié)點(diǎn)同樣也是。讓他們都不再監(jiān)聽最小節(jié)點(diǎn),而是監(jiān)聽他的上一個(gè)節(jié)點(diǎn)。當(dāng)上一個(gè)節(jié)點(diǎn)釋放鎖后,那當(dāng)前節(jié)點(diǎn)就可以創(chuàng)建寫鎖了。
這里還會遇到一個(gè)問題,假如他的上一個(gè)節(jié)點(diǎn)意外刪除了,但是并不是等著拿到鎖后釋放鎖,而是單純的不想等了,所以刪除了節(jié)點(diǎn),而上面還有很多節(jié)點(diǎn)加著鎖呢,所以我們不能單純的靠上一個(gè)節(jié)點(diǎn)刪除后當(dāng)前節(jié)點(diǎn)就進(jìn)行加鎖。我們加寫鎖要保證的是,他上面沒有任何節(jié)點(diǎn)加鎖。這時(shí)候讓他進(jìn)行監(jiān)聽上上個(gè)節(jié)點(diǎn)即可。假如上上個(gè)節(jié)點(diǎn)仍然有問題了,那就監(jiān)聽上上上個(gè)節(jié)點(diǎn)。總之一點(diǎn)就是盡量避免不讓多個(gè)節(jié)點(diǎn)同時(shí)去監(jiān)聽一個(gè)節(jié)點(diǎn)。
三、springboot整合分布式鎖
springboot整合curator客戶端:http://www.dhdzp.com/article/181082.htm
我直接是基于上一篇文章當(dāng)中的項(xiàng)目進(jìn)行 分布式鎖 練習(xí)的!
根據(jù)上面提到的zk分布式鎖實(shí)現(xiàn)思路,我們其實(shí)并不用去自己寫,在curator客戶端已經(jīng)給我們提供了現(xiàn)成的方法,我們只需要簡單的調(diào)用客戶端提供的方法,就可以實(shí)現(xiàn)分布式鎖功能!
@Autowired
CuratorFramework curatorFramework;
/**
* 獲取讀鎖的條件是前面沒有寫鎖
* 當(dāng)/lock1節(jié)點(diǎn)不存在的時(shí)候,我們不需要手動(dòng)去創(chuàng)建,獲取鎖的時(shí)候會自動(dòng)創(chuàng)建
* 自動(dòng)創(chuàng)建的是臨時(shí)節(jié)點(diǎn),用完之后釋放鎖的時(shí)候會刪除掉的
* 獲取到鎖之后會在/lock1節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)序號節(jié)點(diǎn)
* 然后沒有獲取到鎖的線程也會創(chuàng)建一個(gè)節(jié)點(diǎn),這時(shí)候處于等待期間
* 釋放鎖的時(shí)候,首先會刪除掉自己的序號節(jié)點(diǎn),然后假如沒有人在排隊(duì)用鎖,這時(shí)候會把/lock1節(jié)點(diǎn)也刪除掉
*
* @throws Exception
*/
@Test
void testGetReadLock() throws Exception {
// 讀寫鎖
InterProcessReadWriteLock interProcessReadWriteLock = new
InterProcessReadWriteLock(curatorFramework, "/lock1");
// 獲取讀鎖對象(創(chuàng)建對象耗時(shí)也就18毫秒)
InterProcessLock interProcessLock = interProcessReadWriteLock.readLock();
System.out.println("等待獲取讀鎖對象!");
// 獲取鎖(假如一直沒拿到鎖這個(gè)方法一直會是阻塞的,就算不阻塞的情況下,這個(gè)方法耗時(shí)也是特別長,高達(dá)17秒)
interProcessLock.acquire();
// 正常的我們代碼假如走到了這一步,說明已經(jīng)獲取到鎖了,這里寫相關(guān)的業(yè)務(wù)代碼即可,執(zhí)行完記住釋放鎖
System.out.println("獲取到了鎖!");
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 釋放鎖(方法耗時(shí)13毫秒)
interProcessLock.release();
// 走到這一步的時(shí)候節(jié)點(diǎn)已經(jīng)被刪除了
System.out.println("等待釋放鎖!");
}
/**
* 獲取寫鎖的條件是前面沒有任何的鎖
*
* @throws Exception
*/
@Test
void testGetWriteLock() throws Exception {
// 讀寫鎖
InterProcessReadWriteLock interProcessReadWriteLock = new
InterProcessReadWriteLock(curatorFramework, "/lock1");
// 獲取寫鎖對象
InterProcessLock
interProcessLock = interProcessReadWriteLock.writeLock();
System.out.println("等待獲取寫鎖對象!");
// 獲取鎖(假如一直沒拿到鎖這個(gè)方法一直會是阻塞的)
interProcessLock.acquire();
for (int i = 1; i <= 100; i++) {
Thread.sleep(3000);
System.out.println(i);
}
// 釋放鎖
interProcessLock.release();
System.out.println("等待釋放鎖!");
}
/**
* 方便測試多個(gè)線程獲取讀鎖
*
* @throws Exception
*/
@Test
void testGetReadLock1() throws Exception {
testGetReadLock();
}
這個(gè)是兩個(gè)獲取讀鎖的時(shí)候的節(jié)點(diǎn)場景:

這個(gè)是一個(gè)獲取讀鎖,一個(gè)獲取寫鎖的場景,然后讀鎖節(jié)點(diǎn)是0004,所以是后創(chuàng)建的,他只有等待0003釋放寫鎖,才能獲取到讀鎖。

zk分布式鎖會產(chǎn)生死鎖嗎?
這個(gè)肯定是不會的,因?yàn)榧偃缬锌蛻舳四玫搅随i,還沒釋放,服務(wù)掛了,這時(shí)候依據(jù)臨時(shí)節(jié)點(diǎn)的特性,當(dāng)臨時(shí)節(jié)點(diǎn)和客戶端斷開連接幾秒后會自動(dòng)刪除的,刪除節(jié)點(diǎn)也就意味著自動(dòng)釋放鎖!
正常情況不建議使用zk作為分布式鎖,效率屬實(shí)太慢。
到此這篇關(guān)于Spring Boot整合Zookeeper實(shí)現(xiàn)分布式鎖的文章就介紹到這了,更多相關(guān)Spring Boot整合Zookeeper分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中數(shù)據(jù)傳輸對象(DTO)的實(shí)現(xiàn)
本文主要介紹了SpringBoot中數(shù)據(jù)傳輸對象(DTO)的實(shí)現(xiàn),包括了手動(dòng)創(chuàng)建DTO、使用ModelMapper和Lombok創(chuàng)建DTO的示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07
java實(shí)現(xiàn)動(dòng)態(tài)編譯并動(dòng)態(tài)加載
這篇文章主要介紹了java實(shí)現(xiàn)動(dòng)態(tài)編譯并動(dòng)態(tài)加載,需要的朋友可以參考下2021-04-04
Java實(shí)現(xiàn)調(diào)用ElasticSearch?API的示例詳解
這篇文章主要為大家詳細(xì)介紹了Java調(diào)用ElasticSearch?API的效果資料,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2023-03-03
mybatis如何獲取剛剛新插入數(shù)據(jù)的主鍵值id
這篇文章主要介紹了mybatis如何獲取剛剛新插入數(shù)據(jù)的主鍵值id問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
Java實(shí)現(xiàn)Jar文件的遍歷復(fù)制與文件追加
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)Jar文件的遍歷復(fù)制與文件追加功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11
java中Socket設(shè)置超時(shí)時(shí)間的兩種方式
這篇文章主要介紹了java中Socket設(shè)置超時(shí)時(shí)間的兩種方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11

