分布式遭遇并發(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卤摺)