分布式(1)-- 分布式鎖

分布式遭遇并發(fā)

在前面的章節(jié)槐脏,并發(fā)操作要么發(fā)生在單個(gè)應(yīng)用內(nèi),一般使用基于JVM的lock解決并發(fā)問題甜刻,要么發(fā)生在數(shù)據(jù)庫,可以考慮使用數(shù)據(jù)庫層面的鎖正勒,而在分布式場(chǎng)景下得院,需要保證多個(gè)應(yīng)用實(shí)例都能夠執(zhí)行同步代碼,則需要做一些額外的工作章贞,一個(gè)最典型分布式同步方案便是使用分布式鎖祥绞。

分布式鎖由很多種實(shí)現(xiàn),但本質(zhì)上都是類似的鸭限,即依賴于共享組件實(shí)現(xiàn)鎖的詢問和獲取蜕径,如果說單體式應(yīng)用中的Monitor是由JVM提供的,那么分布式下Monitor便是由共享組件提供败京,而典型的共享組件大家其實(shí)并不陌生兜喻,包括但不限于:Mysql,Redis赡麦,Zookeeper朴皆。同時(shí)他們也代表了三種類型的共享組件:數(shù)據(jù)庫,緩存泛粹,分布式協(xié)調(diào)組件遂铡。基于Consul的分布式鎖晶姊,其實(shí)和基于Zookeeper的分布式鎖大同小異扒接,都是借助于分布式協(xié)調(diào)組件實(shí)現(xiàn)鎖,大而化之帽借,這三種類型的分布式鎖珠增,原理也都差不多,只不過砍艾,鎖的特性和實(shí)現(xiàn)細(xì)節(jié)有所差異蒂教。

Redis實(shí)現(xiàn)分布式鎖

定義需求:A應(yīng)用需要完成添加庫存的操作,部署了A1脆荷,A2凝垛,A3多個(gè)實(shí)例懊悯,實(shí)例之間的操作要保證同步。

分析需求:顯然梦皮,此時(shí)依賴于JVM的lock已經(jīng)沒辦法解決問題了炭分,A1添加鎖,無法保證A2剑肯,A3的同步捧毛,這種場(chǎng)景可以考慮使用分布式鎖應(yīng)對(duì)。

建立一張Stock表让网,包含id呀忧,number兩個(gè)字段,分別讓A1溃睹,A2而账,A3并發(fā)對(duì)其操作,保證線程安全因篇。

@Entity

publicclassStock {

????@Id

????privateString id;

????privateInteger number;

}

定義數(shù)據(jù)庫訪問層:

publicinterfaceStockRepository extendsJpaRepository<Stock,String> {

}

如果你的項(xiàng)目中Redis是多機(jī)部署的泞辐,那么可以嘗試使用Redisson實(shí)現(xiàn)分布式鎖,這是Redis官方提供的Java組件竞滓。

這一節(jié)的主角咐吼,redis分布式鎖,使用開源的redis分布式鎖實(shí)現(xiàn):Redisson虽界。

引入Redisson依賴:

<dependency>

????<groupId>org.redisson</groupId>

????<artifactId>redisson</artifactId>

????<version>3.5.4</version>

</dependency>

定義測(cè)試類:

@RestController

publicclassStockController {

????@Autowired

????StockRepository stockRepository;

????ExecutorService executorService = Executors.newFixedThreadPool(10);

????@Autowired

????RedissonClient redissonClient;

????finalstaticString id = "1";

????@RequestMapping("/addStock")

????publicvoidaddStock() {

????????RLock lock = redissonClient.getLock("redisson:lock:stock:"+ id);

????????for(inti = 0; i < 100; i++) {

????????????executorService.execute(() -> {

????????????????lock.lock();

????????????????try{

????????????????????Stock stock = stockRepository.findOne(id);

????????????????????stock.setNumber(stock.getNumber() + 1);

????????????????????stockRepository.save(stock);

????????????????} finally{

????????????????????lock.unlock();

????????????????}

????????????});

????????}

????}

}

上述的代碼使得并發(fā)發(fā)生在多個(gè)層面汽烦。其一,在應(yīng)用內(nèi)部莉御,啟用線程池完成庫存的加1操作撇吞,本身便是線程不安全的,其二礁叔,在多個(gè)應(yīng)用之間牍颈,這樣的加1操作更加是不受約束的。若初始化id為1的Stock數(shù)量為0琅关。分別在本地啟用A1(8080)党瓮,A2(8081)盔憨,A3(8082)三個(gè)應(yīng)用,同時(shí)并發(fā)執(zhí)行一次addStock(),若線程安全聚凹,必然可以使得數(shù)據(jù)庫中的Stock為300摩钙,這便是我們的檢測(cè)依據(jù)筑凫。

簡(jiǎn)單解讀下上述的代碼虐块,使用redisson獲取一把RLock,RLock是java.util.concurrent.locks.Lock接口的實(shí)現(xiàn)類徒爹,Redisson幫助我們屏蔽Redis分布式鎖的實(shí)現(xiàn)細(xì)節(jié)荚醒,使用過java.util.concurrent.locks.Lock的朋友都會(huì)知道下述的代碼可以被稱得上是同步的起手范式芋类,畢竟這是Lock的java

doc中給出的代碼:

Lock l = ...;

l.lock();

try{

???// access the resource protected by this lock

} finally{

??l.unlock();

}

而redissonClient.getLock(“redisson:lock:stock:” + id)則是以”redisson:lock:stock:” + id該字符串作痛同步的Monitor,保證了不同id之間是互相不阻塞的界阁。

為了保證發(fā)生并發(fā)侯繁,實(shí)際測(cè)試中我加入了Thread.sleep(1000),使競(jìng)爭(zhēng)得以發(fā)生泡躯。測(cè)試結(jié)果:

Redis分布式鎖的確起了作用贮竟。

鎖的注意點(diǎn)

如果僅僅是實(shí)現(xiàn)一個(gè)能夠用于demo的Redis分布式鎖并不難,但為何大家更偏向于使用開源的實(shí)現(xiàn)呢精续?主要還是可用性和穩(wěn)定性坝锰,we make

things

work是我在寫博客,寫代碼時(shí)牢記在腦海中的重付,如果真的要細(xì)究如何自己實(shí)現(xiàn)一個(gè)分布式鎖,或者平時(shí)使用鎖保證并發(fā)凫乖,需要有哪些注意點(diǎn)呢确垫?列舉幾點(diǎn):阻塞,超時(shí)時(shí)間帽芽,可重入删掀,可用性,其他特性导街。

阻塞

意味著各個(gè)操作之間的等待披泪,A1正在執(zhí)行增加庫存時(shí),A1其他的線程被阻塞搬瑰,A2款票,A3中所有的線程被阻塞,在Redis中可以使用輪詢策略以及redis底層提供的CAS原語(如setnx)來實(shí)現(xiàn)泽论。(初學(xué)者可以理解為:在redis中設(shè)置一個(gè)key艾少,想要執(zhí)行l(wèi)ock代碼時(shí)先詢問是否有該key,如果有則代表其他線程在執(zhí)行過程中翼悴,若沒有缚够,則設(shè)置該key,并且執(zhí)行代碼鹦赎,執(zhí)行完畢谍椅,釋放key,而setnx保證操作的原子性)

超時(shí)時(shí)間

在特殊情況古话,可能會(huì)導(dǎo)致鎖無法被釋放雏吭,如死鎖,死循環(huán)等等意料之外的情況煞额,鎖超時(shí)時(shí)間的設(shè)置是有必要的思恐,一個(gè)很直觀的想法是給key設(shè)置過期時(shí)間即可沾谜。

如在Redisson中,lock提供了一個(gè)重載方法lock(long t, TimeUnit timeUnit);可以自定義過期時(shí)間胀莹。

可重入

這個(gè)特性很容易被忽視基跑,可重入其實(shí)并不難理解,顧名思義描焰,一個(gè)方法在調(diào)用過程中是否可以被再次調(diào)用媳否。實(shí)現(xiàn)可重入需要滿足三個(gè)特性:

可以在執(zhí)行的過程中可以被打斷;

被打斷之后荆秦,在該函數(shù)一次調(diào)用執(zhí)行完之前篱竭,可以再次被調(diào)用(或進(jìn)入,reentered)步绸。

再次調(diào)用執(zhí)行完之后掺逼,被打斷的上次調(diào)用可以繼續(xù)恢復(fù)執(zhí)行,并正確執(zhí)行瓤介。

比如下述的代碼引用了全局變量吕喘,便是不可重入的:

int t;

voidswap(intx, inty) {

????t = x;

????x = y;

????y = t;

????System.out.println("x is"+ x + " y is "+ y);

}

一個(gè)更加直觀的例子便是,同一個(gè)線程中刑桑,某個(gè)方法的遞歸調(diào)用不應(yīng)該被阻塞氯质,所以如果要實(shí)現(xiàn)這個(gè)特性,簡(jiǎn)單的使用某個(gè)key作為Monitor是欠妥的祠斧,可以加入線程編號(hào)闻察,來保證可重入。

使用可重入分布式鎖的來測(cè)試計(jì)算斐波那契數(shù)列(只是為了驗(yàn)證可重入性):

@RequestMapping("testReentrant")

publicvoidReentrantLock() {

????RLock lock = redissonClient.getLock("fibonacci");

????lock.lock();

????try{

????????intresult = fibonacci(10);

????????System.out.println(result);

????} finally{

????????lock.unlock();

????}

}

intfibonacci(intn) {

????RLock lock = redissonClient.getLock("fibonacci");

????try{

????????if(n <= 1) returnn;

????????else

????????????returnfibonacci(n - 1) + fibonacci(n - 2);

????} finally{

????????lock.unlock();

????}

}

最終輸出:55琢锋,可以發(fā)現(xiàn)辕漂,只要是在同一線程之內(nèi),無論是遞歸調(diào)用還是外部加鎖(同一把鎖)吩蔑,都不會(huì)造成死鎖钮热。

可用性

借助于第三方中間件實(shí)現(xiàn)的分布式鎖,都有這個(gè)問題烛芬,中間件掛了隧期,會(huì)導(dǎo)致鎖不可用,所以需要保證鎖的高可用赘娄,這就需要保證中間件的可用性仆潮,如redis可以使用哨兵+集群,保證了中間件的可用性遣臼,便保證了鎖的可用性性置、

其他特性

除了可重入鎖,鎖的分類還有很多揍堰,在分布式下也同樣可以實(shí)現(xiàn)鹏浅,包括但不限于:公平鎖嗅义,聯(lián)鎖,信號(hào)量隐砸,讀寫鎖之碗。Redisson也都提供了相關(guān)的實(shí)現(xiàn)類,其他的特性如并發(fā)容器等可以參考官方文檔季希。

新手遭遇并發(fā)

基本算是把項(xiàng)目中遇到的并發(fā)過了一遍了褪那,案例其實(shí)很多,再簡(jiǎn)單羅列下一些新手可能會(huì)遇到的問題式塌。

使用了線程安全的容器就是線程安全了嗎博敬?很多新手誤以為使用了并發(fā)容器如:concurrentHashMap就萬事大吉了,卻不知道峰尝,一知半解的隱患可能比全然不懂更大偏窝。來看下面的代碼:

publicclassConcurrentHashMapTest {

????staticMap<String, Integer> counter = newConcurrentHashMap();

????publicstaticvoidmain(String[] args) throwsInterruptedException {

????????counter.put("stock1", 0);

????????ExecutorService executorService = Executors.newFixedThreadPool(10);

????????CountDownLatch countDownLatch = newCountDownLatch(100);

????????for(inti = 0; i < 100; i++) {

????????????executorService.execute(newRunnable() {

????????????????@Override

????????????????publicvoidrun() {

????????????????????counter.put("stock1", counter.get("stock1") + 1);

????????????????????countDownLatch.countDown();

????????????????}

????????????});

????????}

????????countDownLatch.await();

????????System.out.println("result is "+ counter.get("stock1"));

????}

}

counter.put(“stock1″, counter.get(“stock1″) + 1)并不是原子操作,并發(fā)容器保證的是單步操作的線程安全特性境析,這一點(diǎn)往往初級(jí)程序員特別容易忽視囚枪。

總結(jié)

項(xiàng)目中的并發(fā)場(chǎng)景是非常多的,而根據(jù)場(chǎng)景不同劳淆,同一個(gè)場(chǎng)景下的業(yè)務(wù)需求不同,以及數(shù)據(jù)量默赂,訪問量的不同沛鸵,都會(huì)影響到鎖的使用,架構(gòu)中經(jīng)常被提到的一句話是:業(yè)務(wù)決定架構(gòu)缆八,放到并發(fā)中也同樣適用:業(yè)務(wù)決定控制并發(fā)的手段曲掰,如本文未涉及的隊(duì)列的使用,本質(zhì)上是化并發(fā)為串行奈辰,也解決了并發(fā)問題栏妖,都是控制的手段。了解鎖的使用很簡(jiǎn)單奖恰,但如果使用吊趾,在什么場(chǎng)景下使用什么樣的鎖,這才是價(jià)值所在瑟啃。

同一個(gè)線程之間的遞歸調(diào)用不應(yīng)該被阻塞论泛,所以如果要實(shí)現(xiàn)這個(gè)特性,簡(jiǎn)單的使用某個(gè)key作為Monitor是欠妥的蛹屿,可以加入線程編號(hào)屁奏,來保證可重入。


(原文地址:http://www.importnew.com/27278.html 错负。 尊重原創(chuàng)坟瓢,感謝作者S卤摺)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市折联,隨后出現(xiàn)的幾起案子粒褒,更是在濱河造成了極大的恐慌,老刑警劉巖崭庸,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怀浆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡怕享,警方通過查閱死者的電腦和手機(jī)执赡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來函筋,“玉大人沙合,你說我怎么就攤上這事〉剩” “怎么了首懈?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谨敛。 經(jīng)常有香客問我究履,道長(zhǎng),這世上最難降的妖魔是什么脸狸? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任最仑,我火速辦了婚禮,結(jié)果婚禮上炊甲,老公的妹妹穿的比我還像新娘泥彤。我一直安慰自己,他們只是感情好卿啡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布吟吝。 她就那樣靜靜地躺著,像睡著了一般颈娜。 火紅的嫁衣襯著肌膚如雪剑逃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天揭鳞,我揣著相機(jī)與錄音炕贵,去河邊找鬼。 笑死野崇,一個(gè)胖子當(dāng)著我的面吹牛称开,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鳖轰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼清酥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蕴侣,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤焰轻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后昆雀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辱志,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狞膘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了揩懒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挽封,死狀恐怖已球,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辅愿,我是刑警寧澤智亮,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站点待,受9級(jí)特大地震影響阔蛉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜癞埠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一馍忽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧燕差,春花似錦、人聲如沸坝冕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喂窟。三九已至测暗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間磨澡,已是汗流浹背碗啄。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稳摄,地道東北人稚字。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親胆描。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瘫想,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容