三萬(wàn)字:架構(gòu)+源碼深度解析分布式鎖架構(gòu)原理與實(shí)現(xiàn)方案
大家好,我是冰河~~
最近,很多小伙伴留言說(shuō),在學(xué)習(xí)高并發(fā)編程時(shí),不太明白分布式鎖是用來(lái)解決什么問(wèn)題的,還有不少小伙伴甚至連分布式鎖是什么都不太明白。
明明在生產(chǎn)環(huán)境上使用了自己開(kāi)發(fā)的分布式鎖,為什么還會(huì)出現(xiàn)問(wèn)題呢?同樣的程序,加上分布式鎖后,性能差了幾個(gè)數(shù)量級(jí)!這又是為什么呢?
今天,我們就來(lái)說(shuō)說(shuō)如何在高并發(fā)環(huán)境下實(shí)現(xiàn)分布式鎖,不是所有的鎖都是高并發(fā)的。
萬(wàn)字長(zhǎng)文,帶你深入解密高并發(fā)環(huán)境下的分布式鎖架構(gòu),不是所有的鎖都是分布式鎖。
究竟什么樣的鎖才能更好的支持高并發(fā)場(chǎng)景呢?今天,我們就一起解密高并發(fā)環(huán)境下典型的分布式鎖架構(gòu),結(jié)合【高并發(fā)】專題下的其他文章,學(xué)以致用。
鎖用來(lái)解決什么問(wèn)題呢?
在我們編寫(xiě)的應(yīng)用程序或者高并發(fā)程序中,不知道大家有沒(méi)有想過(guò)一個(gè)問(wèn)題,就是我們?yōu)槭裁葱枰腈i?鎖為我們解決了什么問(wèn)題呢?
在很多業(yè)務(wù)場(chǎng)景下,我們編寫(xiě)的應(yīng)用程序中會(huì)存在很多的 資源競(jìng)爭(zhēng) 的問(wèn)題。而我們?cè)诟卟l(fā)程序中,引入鎖,就是為了解決這些資源競(jìng)爭(zhēng)的問(wèn)題。
電商超賣(mài)問(wèn)題
這里,我們可以列舉一個(gè)簡(jiǎn)單的業(yè)務(wù)場(chǎng)景。比如,在電子商務(wù)(商城)的業(yè)務(wù)場(chǎng)景中,提交訂單購(gòu)買(mǎi)商品時(shí),首先需要查詢相應(yīng)商品的庫(kù)存是否足夠,只有在商品庫(kù)存數(shù)量足夠的前提下,才能讓用戶成功的下單。
下單時(shí),我們需要在庫(kù)存數(shù)量中減去用戶下單的商品數(shù)量,并將庫(kù)存操作的結(jié)果數(shù)據(jù)更新到數(shù)據(jù)庫(kù)中。整個(gè)流程我們可以簡(jiǎn)化成下圖所示。
很多小伙伴也留言說(shuō),讓我給出代碼,這樣能夠更好的學(xué)習(xí)和掌握相關(guān)的知識(shí)。好吧,這里,我也給出相應(yīng)的代碼片段吧。
我們可以使用下面的代碼片段來(lái)表示用戶的下單操作,我這里將商品的庫(kù)存信息保存在了Redis中。
@RequestMapping("/submitOrder")
public String submitOrder(){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
stock -= 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
logger.debug("庫(kù)存扣減成功,當(dāng)前庫(kù)存為:{}", stock);
}else{
logger.debug("庫(kù)存不足,扣減庫(kù)存失敗");
throw new OrderException("庫(kù)存不足,扣減庫(kù)存失敗");
}
return "success";
}
注意:上述代碼片段比較簡(jiǎn)單,只是為了方便大家理解,真正項(xiàng)目中的代碼就不能這么寫(xiě)了。
上述的代碼看似是沒(méi)啥問(wèn)題的,但是我們不能只從代碼表面上來(lái)觀察代碼的執(zhí)行順序。這是因?yàn)樵贘VM中代碼的執(zhí)行順序未必是按照我們書(shū)寫(xiě)代碼的順序執(zhí)行的。
即使在JVM中代碼是按照我們書(shū)寫(xiě)的順序執(zhí)行,那我們對(duì)外提供的接口一旦暴露出去,就會(huì)有成千上萬(wàn)的客戶端來(lái)訪問(wèn)我們的接口。所以說(shuō),我們暴露出去的接口是會(huì)被并發(fā)訪問(wèn)的。
試問(wèn),上面的代碼在高并發(fā)環(huán)境下是線程安全的嗎?答案肯定不是線程安全的,因?yàn)樯鲜隹蹨p庫(kù)存的操作會(huì)出現(xiàn)并行執(zhí)行的情況。
我們可以使用Apache JMeter來(lái)對(duì)上述接口進(jìn)行測(cè)試,這里,我使用Apache JMeter對(duì)上述接口進(jìn)行測(cè)試。
在Jmeter中,我將線程的并發(fā)度設(shè)置為3,接下來(lái)的配置如下所示。
以HTTP GET請(qǐng)求的方式來(lái)并發(fā)訪問(wèn)提交訂單的接口。此時(shí),運(yùn)行JMeter來(lái)訪問(wèn)接口,命令行會(huì)打印出下面的日志信息。
庫(kù)存扣減成功,當(dāng)前庫(kù)存為:49
庫(kù)存扣減成功,當(dāng)前庫(kù)存為:49
庫(kù)存扣減成功,當(dāng)前庫(kù)存為:49
這里,我們明明請(qǐng)求了3次,也就是說(shuō),提交了3筆訂單,為什么扣減后的庫(kù)存都是一樣的呢?這種現(xiàn)象在電商領(lǐng)域有一個(gè)專業(yè)的名詞叫做 “超賣(mài)” 。
如果一個(gè)大型的高并發(fā)電商系統(tǒng),比如淘寶、天貓、京東等,出現(xiàn)了超賣(mài)現(xiàn)象,那損失就無(wú)法估量了!架構(gòu)設(shè)計(jì)和開(kāi)發(fā)電商系統(tǒng)的人員估計(jì)就要通通下崗了。所以,作為技術(shù)人員,我們一定要嚴(yán)謹(jǐn)?shù)膶?duì)待技術(shù),嚴(yán)格做好系統(tǒng)的每一個(gè)技術(shù)環(huán)節(jié)。
JVM中提供的鎖
JVM中提供的synchronized和Lock鎖,相信大家并不陌生了,很多小伙伴都會(huì)使用這些鎖,也能使用這些鎖來(lái)實(shí)現(xiàn)一些簡(jiǎn)單的線程互斥功能。那么,作為立志要成為架構(gòu)師的你,是否了解過(guò)JVM鎖的底層原理呢?
JVM鎖原理
說(shuō)到JVM鎖的原理,我們就不得不限說(shuō)說(shuō)Java中的對(duì)象頭了。
Java中的對(duì)象頭
每個(gè)Java對(duì)象都有對(duì)象頭。如果是?數(shù)組類型,則?2個(gè)字寬來(lái)存儲(chǔ)對(duì)象頭,如果是數(shù)組,則會(huì)?3個(gè)字寬來(lái)存儲(chǔ)對(duì)象頭。在32位處理器中,?個(gè)字寬是32位;在64位虛擬機(jī)中,?個(gè)字寬是64位。
對(duì)象頭的內(nèi)容如下表 。
長(zhǎng)度 | 內(nèi)容 | 說(shuō)明 |
32/64bit | Mark Word | 存儲(chǔ)對(duì)象的hashCode或鎖信息等 |
32/64bit | Class Metadata Access | 存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針 |
32/64bit | Array length | 數(shù)組的長(zhǎng)度(如果是數(shù)組) |
Mark Work的格式如下所示。
鎖狀態(tài) | 29bit或61bit | 1bit是否是偏向鎖? | 2bit鎖標(biāo)志位 |
無(wú)鎖 | 0 | 01 | |
偏向鎖 | 線程ID | 1 | 01 |
輕量級(jí)鎖 | 指向棧中鎖記錄的指針 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 00 |
重量級(jí)鎖 | 指向互斥量(重量級(jí)鎖)的指針 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 10 |
GC標(biāo)記 | 此時(shí)這一位不用于標(biāo)識(shí)偏向鎖 | 11 |
可以看到,當(dāng)對(duì)象狀態(tài)為偏向鎖時(shí), Mark Word 存儲(chǔ)的是偏向的線程ID;當(dāng)狀態(tài)為輕量級(jí)鎖時(shí), Mark Word 存儲(chǔ)的是指向線程棧中 Lock Record 的指針;當(dāng)狀態(tài)為重量級(jí)鎖時(shí), Mark Word 為指向堆中的monitor對(duì)象的指針 。
有關(guān)Java對(duì)象頭的知識(shí),參考《深入淺出Java多線程》。
JVM鎖原理
簡(jiǎn)單點(diǎn)來(lái)說(shuō),JVM中鎖的原理如下。
在Java對(duì)象的對(duì)象頭上,有一個(gè)鎖的標(biāo)記,比如,第一個(gè)線程執(zhí)行程序時(shí),檢查Java對(duì)象頭中的鎖標(biāo)記,發(fā)現(xiàn)Java對(duì)象頭中的鎖標(biāo)記為未加鎖狀態(tài),于是為Java對(duì)象進(jìn)行了加鎖操作,將對(duì)象頭中的鎖標(biāo)記設(shè)置為鎖定狀態(tài)。
第二個(gè)線程執(zhí)行同樣的程序時(shí),也會(huì)檢查Java對(duì)象頭中的鎖標(biāo)記,此時(shí)會(huì)發(fā)現(xiàn)Java對(duì)象頭中的鎖標(biāo)記的狀態(tài)為鎖定狀態(tài)。于是,第二個(gè)線程會(huì)進(jìn)入相應(yīng)的阻塞隊(duì)列中進(jìn)行等待。
這里有一個(gè)關(guān)鍵點(diǎn)就是Java對(duì)象頭中的鎖標(biāo)記如何實(shí)現(xiàn)。
JVM鎖的短板
JVM中提供的synchronized和Lock鎖都是JVM級(jí)別的,大家都知道,當(dāng)運(yùn)行一個(gè)Java程序時(shí),會(huì)啟動(dòng)一個(gè)JVM進(jìn)程來(lái)運(yùn)行我們的應(yīng)用程序。synchronized和Lock在JVM級(jí)別有效。
也就是說(shuō),synchronized和Lock在同一Java進(jìn)程內(nèi)有效。如果我們開(kāi)發(fā)的應(yīng)用程序是分布式的,那么只是使用synchronized和Lock來(lái)解決分布式場(chǎng)景下的高并發(fā)問(wèn)題,就會(huì)顯得有點(diǎn)力不從心了。
synchronized和Lock支持JVM同一進(jìn)程內(nèi)部的線程互斥
synchronized和Lock在JVM級(jí)別能夠保證高并發(fā)程序的互斥,我們可以使用下圖來(lái)表示。
但是,當(dāng)我們將應(yīng)用程序部署成分布式架構(gòu),或者將應(yīng)用程序在不同的JVM進(jìn)程中運(yùn)行時(shí),synchronized和Lock就不能保證分布式架構(gòu)和多JVM進(jìn)程下應(yīng)用程序的互斥性了。
synchronized和Lock不能實(shí)現(xiàn)多JVM進(jìn)程之間的線程互斥
分布式架構(gòu)和多JVM進(jìn)程的本質(zhì)都是將應(yīng)用程序部署在不同的JVM實(shí)例中,也就是說(shuō),其本質(zhì)還是多JVM進(jìn)程。
分布式鎖
我們?cè)趯?shí)現(xiàn)分布式鎖時(shí),可以參照J(rèn)VM鎖實(shí)現(xiàn)的思想,JVM鎖在為對(duì)象加鎖時(shí),通過(guò)改變Java對(duì)象的對(duì)象頭中的鎖的標(biāo)志位來(lái)實(shí)現(xiàn),也就是說(shuō),所有的線程都會(huì)訪問(wèn)這個(gè)Java對(duì)象的對(duì)象頭中的鎖標(biāo)志位。
我們同樣以這種思想來(lái)實(shí)現(xiàn)分布式鎖,當(dāng)我們將應(yīng)用程序進(jìn)行拆分并部署成分布式架構(gòu)時(shí),所有應(yīng)用程序中的線程訪問(wèn)共享變量時(shí),都到同一個(gè)地方去檢查當(dāng)前程序的臨界區(qū)是否進(jìn)行了加鎖操作,而是否進(jìn)行了加鎖操作,我們?cè)诮y(tǒng)一的地方使用相應(yīng)的狀態(tài)來(lái)進(jìn)行標(biāo)記。
可以看到,在分布式鎖的實(shí)現(xiàn)思想上,與JVM鎖相差不大。而在實(shí)現(xiàn)分布式鎖中,保存加鎖狀態(tài)的服務(wù)可以使用MySQL、Redis和Zookeeper實(shí)現(xiàn)。
但是,在互聯(lián)網(wǎng)高并發(fā)環(huán)境中, 使用Redis實(shí)現(xiàn)分布式鎖的方案是使用的最多的。 接下來(lái),我們就使用Redis來(lái)深入解密分布式鎖的架構(gòu)設(shè)計(jì)。
Redis如何實(shí)現(xiàn)分布式鎖
Redis命令
在Redis中,有一個(gè)不常使用的命令如下所示。
SETNX key value
這條命令的含義就是“SET if Not Exists”,即不存在的時(shí)候才會(huì)設(shè)置值。
只有在key不存在的情況下,將鍵key的值設(shè)置為value。如果key已經(jīng)存在,則SETNX命令不做任何操作。
這個(gè)命令的返回值如下。
- 命令在設(shè)置成功時(shí)返回1。
- 命令在設(shè)置失敗時(shí)返回0。
所以,我們?cè)诜植际礁卟l(fā)環(huán)境下,可以使用Redis的SETNX命令來(lái)實(shí)現(xiàn)分布式鎖。假設(shè)此時(shí)有線程A和線程B同時(shí)訪問(wèn)臨界區(qū)代碼,假設(shè)線程A首先執(zhí)行了SETNX命令,并返回結(jié)果1,繼續(xù)向下執(zhí)行。
而此時(shí)線程B再次執(zhí)行SETNX命令時(shí),返回的結(jié)果為0,則線程B不能繼續(xù)向下執(zhí)行。只有當(dāng)線程A執(zhí)行DELETE命令將設(shè)置的鎖狀態(tài)刪除時(shí),線程B才會(huì)成功執(zhí)行SETNX命令設(shè)置加鎖狀態(tài)后繼續(xù)向下執(zhí)行。
引入分布式鎖
了解了如何使用Redis中的命令實(shí)現(xiàn)分布式鎖后,我們就可以對(duì)下單接口進(jìn)行改造了,加入分布式鎖,如下所示。
/**
* 為了演示方便,我這里就簡(jiǎn)單定義了一個(gè)常量作為商品的id
* 實(shí)際工作中,這個(gè)商品id是前端進(jìn)行下單操作傳遞過(guò)來(lái)的參數(shù)
*/
public static final String PRODUCT_ID = "100001";
@RequestMapping("/submitOrder")
public String submitOrder(){
//通過(guò)stringRedisTemplate來(lái)調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
//實(shí)際上,value可以為任意的字符換
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
//沒(méi)有拿到鎖,返回下單失敗
if(!isLock){
return "failure";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
stock -= 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
logger.debug("庫(kù)存扣減成功,當(dāng)前庫(kù)存為:{}", stock);
}else{
logger.debug("庫(kù)存不足,扣減庫(kù)存失敗");
throw new OrderException("庫(kù)存不足,扣減庫(kù)存失敗");
}
//業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
return "success";
}
那么,在上述代碼中,我們加入了分布式鎖的操作,那上述代碼是否能夠在高并發(fā)場(chǎng)景下保證業(yè)務(wù)的原子性呢?答案是可以保證業(yè)務(wù)的原子性。但是,在實(shí)際場(chǎng)景中,上面實(shí)現(xiàn)分布式鎖的代碼是不可用的?。?/p>
假設(shè)當(dāng)線程A首先執(zhí)行stringRedisTemplate.opsForValue()的setIfAbsent()方法返回true,繼續(xù)向下執(zhí)行,正在執(zhí)行業(yè)務(wù)代碼時(shí),拋出了異常,線程A直接退出了JVM。
此時(shí),stringRedisTemplate.delete(PRODUCT_ID);代碼還沒(méi)來(lái)得及執(zhí)行,之后所有的線程進(jìn)入提交訂單的方法時(shí),調(diào)用stringRedisTemplate.opsForValue()的setIfAbsent()方法都會(huì)返回false。導(dǎo)致后續(xù)的所有下單操作都會(huì)失敗。這就是分布式場(chǎng)景下的死鎖問(wèn)題。
所以,上述代碼中實(shí)現(xiàn)分布式鎖的方式在實(shí)際場(chǎng)景下是不可取的?。?/p>
引入try-finally代碼塊
說(shuō)到這,相信小伙伴們都能夠想到,使用try-finall代碼塊啊,接下來(lái),我們?yōu)橄聠谓涌诘姆椒由蟭ry-finally代碼塊。
/**
* 為了演示方便,我這里就簡(jiǎn)單定義了一個(gè)常量作為商品的id
* 實(shí)際工作中,這個(gè)商品id是前端進(jìn)行下單操作傳遞過(guò)來(lái)的參數(shù)
*/
public static final String PRODUCT_ID = "100001";
@RequestMapping("/submitOrder")
public String submitOrder(){
//通過(guò)stringRedisTemplate來(lái)調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
//實(shí)際上,value可以為任意的字符換
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
//沒(méi)有拿到鎖,返回下單失敗
if(!isLock){
return "failure";
}
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
stock -= 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
logger.debug("庫(kù)存扣減成功,當(dāng)前庫(kù)存為:{}", stock);
}else{
logger.debug("庫(kù)存不足,扣減庫(kù)存失敗");
throw new OrderException("庫(kù)存不足,扣減庫(kù)存失敗");
}
}finally{
//業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
}
return "success";
}
那么,上述代碼是否真正解決了死鎖的問(wèn)題呢?我們?cè)趯?xiě)代碼時(shí),不能只盯著代碼本身,覺(jué)得上述代碼沒(méi)啥問(wèn)題了。
實(shí)際上,生產(chǎn)環(huán)境是非常復(fù)雜的。如果線程在成功加鎖之后,執(zhí)行業(yè)務(wù)代碼時(shí),還沒(méi)來(lái)得及執(zhí)行刪除鎖標(biāo)志的代碼。
此時(shí),服務(wù)器宕機(jī)了,程序并沒(méi)有優(yōu)雅的退出JVM。也會(huì)使得后續(xù)的線程進(jìn)入提交訂單的方法時(shí),因無(wú)法成功的設(shè)置鎖標(biāo)志位而下單失敗。所以說(shuō),上述的代碼仍然存在問(wèn)題。
引入Redis超時(shí)機(jī)制
在Redis中可以設(shè)置緩存的自動(dòng)過(guò)期時(shí)間,我們可以將其引入到分布式鎖的實(shí)現(xiàn)中,如下代碼所示。
/**
* 為了演示方便,我這里就簡(jiǎn)單定義了一個(gè)常量作為商品的id
* 實(shí)際工作中,這個(gè)商品id是前端進(jìn)行下單操作傳遞過(guò)來(lái)的參數(shù)
*/
public static final String PRODUCT_ID = "100001";
@RequestMapping("/submitOrder")
public String submitOrder(){
//通過(guò)stringRedisTemplate來(lái)調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
//實(shí)際上,value可以為任意的字符換
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
//沒(méi)有拿到鎖,返回下單失敗
if(!isLock){
return "failure";
}
try{
stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
stock -= 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
logger.debug("庫(kù)存扣減成功,當(dāng)前庫(kù)存為:{}", stock);
}else{
logger.debug("庫(kù)存不足,扣減庫(kù)存失敗");
throw new OrderException("庫(kù)存不足,扣減庫(kù)存失敗");
}
}finally{
//業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
}
return "success";
}
在上述代碼中,我們加入了如下一行代碼來(lái)為Redis中的鎖標(biāo)志設(shè)置過(guò)期時(shí)間。
stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);
此時(shí),我們?cè)O(shè)置的過(guò)期時(shí)間為30秒。
那么問(wèn)題來(lái)了,這樣是否就真正的解決了問(wèn)題呢?上述程序就真的沒(méi)有坑了嗎?答案是還是有坑的?。?/p>
“坑位”分析
我們?cè)谙聠尾僮鞯姆椒ㄖ袨榉植际芥i引入了超時(shí)機(jī)制,此時(shí)的代碼還是無(wú)法真正避免死鎖的問(wèn)題,那“坑位”到底在哪里呢?試想,當(dāng)程序執(zhí)行完stringRedisTemplate.opsForValue().setIfAbsent()方法后,正要執(zhí)行stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS)代碼時(shí),服務(wù)器宕機(jī)了,你還別說(shuō),生產(chǎn)壞境的情況非常復(fù)雜,就是這么巧,服務(wù)器就宕機(jī)了。
此時(shí),后續(xù)請(qǐng)求進(jìn)入提交訂單的方法時(shí),都會(huì)因?yàn)闊o(wú)法成功設(shè)置鎖標(biāo)志而導(dǎo)致后續(xù)下單流程無(wú)法正常執(zhí)行。
既然我們找到了上述代碼的“坑位”,那我們?nèi)绾螌⑦@個(gè)”坑“填上?如何解決這個(gè)問(wèn)題呢?別急,Redis已經(jīng)提供了這樣的功能。我們可以在向Redis中保存數(shù)據(jù)的時(shí)候,可以同時(shí)指定數(shù)據(jù)的超時(shí)時(shí)間。所以,我們可以將代碼改造成如下所示。
/**
* 為了演示方便,我這里就簡(jiǎn)單定義了一個(gè)常量作為商品的id
* 實(shí)際工作中,這個(gè)商品id是前端進(jìn)行下單操作傳遞過(guò)來(lái)的參數(shù)
*/
public static final String PRODUCT_ID = "100001";
@RequestMapping("/submitOrder")
public String submitOrder(){
//通過(guò)stringRedisTemplate來(lái)調(diào)用Redis的SETNX命令,key為商品的id,value為字符串“binghe”
//實(shí)際上,value可以為任意的字符換
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS);
//沒(méi)有拿到鎖,返回下單失敗
if(!isLock){
return "failure";
}
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
stock -= 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
logger.debug("庫(kù)存扣減成功,當(dāng)前庫(kù)存為:{}", stock);
}else{
logger.debug("庫(kù)存不足,扣減庫(kù)存失敗");
throw new OrderException("庫(kù)存不足,扣減庫(kù)存失敗");
}
}finally{
//業(yè)務(wù)執(zhí)行完成,刪除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
}
return "success";
}
在上述代碼中,我們?cè)谙騌edis中設(shè)置鎖標(biāo)志位的時(shí)候就設(shè)置了超時(shí)時(shí)間。此時(shí),只要向Redis中成功設(shè)置了數(shù)據(jù),則即使我們的業(yè)務(wù)系統(tǒng)宕機(jī),Redis中的數(shù)據(jù)過(guò)期后,也會(huì)自動(dòng)刪除。
后續(xù)的線程進(jìn)入提交訂單的方法后,就會(huì)成功的設(shè)置鎖標(biāo)志位,并向下執(zhí)行正常的下單流程。
到此,上述的代碼基本上在功能角度解決了程序的死鎖問(wèn)題,那么,上述程序真的就完美了嗎?哈哈,很多小伙伴肯定會(huì)說(shuō)不完美!確實(shí),上面的代碼還不是完美的,那大家知道哪里不完美嗎?接下來(lái),我們繼續(xù)分析。
在開(kāi)發(fā)集成角度分析代碼
在我們開(kāi)發(fā)公共的系統(tǒng)組件時(shí),比如我們這里說(shuō)的分布式鎖,我們肯定會(huì)抽取一些公共的類來(lái)完成相應(yīng)的功能來(lái)供系統(tǒng)使用。
這里,假設(shè)我們定義了一個(gè)RedisLock接口,如下所示。
public interface RedisLock{
//加鎖操作
boolean tryLock(String key, long timeout, TimeUnit unit);
//解鎖操作
void releaseLock(String key);
}
接下來(lái),使用RedisLockImpl類實(shí)現(xiàn)RedisLock接口,提供具體的加鎖和解鎖實(shí)現(xiàn),如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
return stringRedisTemplate.opsForValue().setIfAbsent(key, "binghe", timeout, unit);
}
@Override
public void releaseLock(String key){
stringRedisTemplate.delete(key);
}
}
在開(kāi)發(fā)集成的角度來(lái)說(shuō),當(dāng)一個(gè)線程從上到下執(zhí)行時(shí),首先對(duì)程序進(jìn)行加鎖操作,然后執(zhí)行業(yè)務(wù)代碼,執(zhí)行完成后,再進(jìn)行釋放鎖的操作。理論上,加鎖和釋放鎖時(shí),操作的Redis Key都是一樣的。
但是,如果其他開(kāi)發(fā)人員在編寫(xiě)代碼時(shí),并沒(méi)有調(diào)用tryLock()方法,而是直接調(diào)用了releaseLock()方法,并且他調(diào)用releaseLock()方法傳遞的key與你調(diào)用tryLock()方法傳遞的key是一樣的。那此時(shí)就會(huì)出現(xiàn)問(wèn)題了,他在編寫(xiě)代碼時(shí),硬生生的將你加的鎖釋放了?。?!
所以,上述代碼是不安全的,別人能夠隨隨便便的將你加的鎖刪除,這就是鎖的誤刪操作,這是非常危險(xiǎn)的,所以,上述的程序存在很?chē)?yán)重的問(wèn)題??!
那如何實(shí)現(xiàn)只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作呢? 繼續(xù)向下看。
如何實(shí)現(xiàn)加鎖和解鎖的歸一化?
什么是加鎖和解鎖的歸一化呢?簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是一個(gè)線程執(zhí)行了加鎖操作后,后續(xù)必須由這個(gè)線程執(zhí)行解鎖操作,加鎖和解鎖操作由同一個(gè)線程來(lái)完成。
為了解決只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作的問(wèn)題,那么,我們就需要將加鎖和解鎖操作綁定到同一個(gè)線程中,那么,如何將加鎖操作和解鎖操作綁定到同一個(gè)線程呢?其實(shí)很簡(jiǎn)單,相信很多小伙伴都想到了—— 使用ThreadLocal實(shí)現(xiàn) 。沒(méi)錯(cuò),使用ThreadLocal類確實(shí)能夠解決這個(gè)問(wèn)題。
此時(shí),我們將RedisLockImpl類的代碼修改成如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
上述代碼的主要邏輯為:在對(duì)程序執(zhí)行嘗試加鎖操作時(shí),首先生成一個(gè)uuid,將生成的uuid綁定到當(dāng)前線程,并將傳遞的key參數(shù)操作Redis中的key,生成的uuid作為Redis中的Value,保存到Redis中,同時(shí)設(shè)置超時(shí)時(shí)間。
當(dāng)執(zhí)行解鎖操作時(shí),首先,判斷當(dāng)前線程中綁定的uuid是否和Redis中存儲(chǔ)的uuid相等,只有二者相等時(shí),才會(huì)執(zhí)行刪除鎖標(biāo)志位的操作。這就避免了一個(gè)線程對(duì)程序進(jìn)行了加鎖操作后,其他線程對(duì)這個(gè)鎖進(jìn)行了解鎖操作的問(wèn)題。
繼續(xù)分析
我們將加鎖和解鎖的方法改成如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private String lockUUID;
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
lockUUID = uuid;
return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(lockUUID.equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
相信很多小伙伴都會(huì)看出上述代碼存在什么問(wèn)題了?。](méi)錯(cuò),那就是 線程安全的問(wèn)題。
所以,這里,我們需要使用ThreadLocal來(lái)解決線程安全問(wèn)題。
可重入性分析
在上面的代碼中,當(dāng)一個(gè)線程成功設(shè)置了鎖標(biāo)志位后,其他的線程再設(shè)置鎖標(biāo)志位時(shí),就會(huì)返回失敗。還有一種場(chǎng)景就是在提交訂單的接口方法中,調(diào)用了服務(wù)A,服務(wù)A調(diào)用了服務(wù)B,而服務(wù)B的方法中存在對(duì)同一個(gè)商品的加鎖和解鎖操作。
所以,服務(wù)B成功設(shè)置鎖標(biāo)志位后,提交訂單的接口方法繼續(xù)執(zhí)行時(shí),也不能成功設(shè)置鎖標(biāo)志位了。也就是說(shuō),目前實(shí)現(xiàn)的分布式鎖沒(méi)有可重入性。
這里,就存在可重入性的問(wèn)題了。我們希望設(shè)計(jì)的分布式鎖 具有可重入性 ,那什么是可重入性呢?簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是同一個(gè)線程,能夠多次獲取同一把鎖,并且能夠按照順序進(jìn)行解決操作。
其實(shí),在JDK 1.5之后提供的鎖很多都支持可重入性,比如synchronized和Lock。
如何實(shí)現(xiàn)可重入性呢?
映射到我們加鎖和解鎖方法時(shí),我們?nèi)绾沃С滞粋€(gè)線程能夠多次獲取到鎖(設(shè)置鎖標(biāo)志位)呢?
可以這樣簡(jiǎn)單的設(shè)計(jì):如果當(dāng)前線程沒(méi)有綁定uuid,則生成uuid綁定到當(dāng)前線程,并且在Redis中設(shè)置鎖標(biāo)志位。
如果當(dāng)前線程已經(jīng)綁定了uuid,則直接返回true,證明當(dāng)前線程之前已經(jīng)設(shè)置了鎖標(biāo)志位,也就是說(shuō)已經(jīng)獲取到了鎖,直接返回true。
結(jié)合以上分析,我們將提交訂單的接口方法代碼改造成如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}else{
isLocked = true;
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
這樣寫(xiě)看似沒(méi)有啥問(wèn)題,但是大家細(xì)想一下,這樣寫(xiě)就真的OK了嗎?
可重入性的問(wèn)題分析
既然上面分布式鎖的可重入性是存在問(wèn)題的,那我們就來(lái)分析下問(wèn)題的根源在哪里!
假設(shè)我們提交訂單的方法中,首先使用RedisLock接口對(duì)代碼塊添加了分布式鎖,在加鎖后的代碼中調(diào)用了服務(wù)A,而服務(wù)A中也存在調(diào)用RedisLock接口的加鎖和解鎖操作。
而多次調(diào)用RedisLock接口的加鎖操作時(shí),只要之前的鎖沒(méi)有失效,則會(huì)直接返回true,表示成功獲取鎖。
也就是說(shuō),無(wú)論調(diào)用加鎖操作多少次,最終只會(huì)成功加鎖一次。而執(zhí)行完服務(wù)A中的邏輯后,在服務(wù)A中調(diào)用RedisLock接口的解鎖方法,此時(shí),會(huì)將當(dāng)前線程所有的加鎖操作獲得的鎖全部釋放掉。
我們可以使用下圖來(lái)簡(jiǎn)單的表示這個(gè)過(guò)程。
那么問(wèn)題來(lái)了,如何解決可重入性的問(wèn)題呢?
解決可重入性問(wèn)題
相信很多小伙伴都能夠想出使用計(jì)數(shù)器的方式來(lái)解決上面可重入性的問(wèn)題,沒(méi)錯(cuò),就是使用計(jì)數(shù)器來(lái)解決。 整體流程如下所示。
那么,體現(xiàn)在程序代碼上是什么樣子呢?我們來(lái)修改RedisLockImpl類的代碼,如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
}else{
isLocked = true;
}
//加鎖成功后將計(jì)數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計(jì)數(shù)器減為0時(shí)釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
至此,我們基本上解決了分布式鎖的可重入性問(wèn)題。
說(shuō)到這里,我還要問(wèn)大家一句,上面的解決問(wèn)題的方案真的沒(méi)問(wèn)題了嗎?
阻塞與非阻塞鎖
在提交訂單的方法中,當(dāng)獲取Redis分布式鎖失敗時(shí),我們直接返回了failure來(lái)表示當(dāng)前請(qǐng)求下單的操作失敗了。
試想,在高并發(fā)環(huán)境下,一旦某個(gè)請(qǐng)求獲得了分布式鎖,那么,在這個(gè)請(qǐng)求釋放鎖之前,其他的請(qǐng)求調(diào)用下單方法時(shí),都會(huì)返回下單失敗的信息。在真實(shí)場(chǎng)景中,這是非常不友好的。
我們可以將后續(xù)的請(qǐng)求進(jìn)行阻塞,直到當(dāng)前請(qǐng)求釋放鎖后,再喚醒阻塞的請(qǐng)求獲得分布式鎖來(lái)執(zhí)行方法。
所以,我們?cè)O(shè)計(jì)的分布式鎖需要支持 阻塞和非阻塞 的特性。
那么,如何實(shí)現(xiàn)阻塞呢?我們可以使用自旋來(lái)實(shí)現(xiàn),繼續(xù)修改RedisLockImpl的代碼如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
//如果獲取鎖失敗,則自旋獲取鎖,直到成功
if(!isLocked){
for(;;){
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
if(isLocked){
break;
}
}
}
}else{
isLocked = true;
}
//加鎖成功后將計(jì)數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計(jì)數(shù)器減為0時(shí)釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
在分布式鎖的設(shè)計(jì)中,阻塞鎖和非阻塞鎖 是非常重要的概念,大家一定要記住這個(gè)知識(shí)點(diǎn)。
鎖失效問(wèn)題
盡管我們實(shí)現(xiàn)了分布式鎖的阻塞特性,但是還有一個(gè)問(wèn)題是我們不得不考慮的。那就是 鎖失效 的問(wèn)題。
當(dāng)程序執(zhí)行業(yè)務(wù)的時(shí)間超過(guò)了鎖的過(guò)期時(shí)間會(huì)發(fā)生什么呢? 想必很多小伙伴都能夠想到,那就是前面的請(qǐng)求沒(méi)執(zhí)行完,鎖過(guò)期失效了,后面的請(qǐng)求獲取到分布式鎖,繼續(xù)向下執(zhí)行了,程序無(wú)法做到真正的互斥,無(wú)法保證業(yè)務(wù)的原子性了。
那如何解決這個(gè)問(wèn)題呢?答案就是:我們必須保證在業(yè)務(wù)代碼執(zhí)行完畢后,才能釋放分布式鎖。 方案是有了,那如何實(shí)現(xiàn)呢?
說(shuō)白了,我們需要在業(yè)務(wù)代碼中,時(shí)不時(shí)的執(zhí)行下面的代碼來(lái)保證在業(yè)務(wù)代碼沒(méi)執(zhí)行完時(shí),分布式鎖不會(huì)因超時(shí)而被釋放。
springRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);
這里,我們需要定義一個(gè)定時(shí)策略來(lái)執(zhí)行上面的代碼,需要注意的是:我們不能等到30秒后再執(zhí)行上述代碼,因?yàn)?0秒時(shí),鎖已經(jīng)失效了。例如,我們可以每10秒執(zhí)行一次上面的代碼。
有些小伙伴說(shuō),直接在RedisLockImpl類中添加一個(gè)while(true)循環(huán)來(lái)解決這個(gè)問(wèn)題,那我們就這樣修改下RedisLockImpl類的代碼,看看有沒(méi)有啥問(wèn)題。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
//如果獲取鎖失敗,則自旋獲取鎖,直到成功
if(!isLocked){
for(;;){
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
if(isLocked){
break;
}
}
}
//定義更新鎖的過(guò)期時(shí)間
while(true){
Integer count = threadLocalInteger.get();
//當(dāng)前鎖已經(jīng)被釋放,則退出循環(huán)
if(count == 0 || count <= 0){
break;
}
springRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
try{
//每隔10秒執(zhí)行一次
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}else{
isLocked = true;
}
//加鎖成功后將計(jì)數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計(jì)數(shù)器減為0時(shí)釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
相信小伙伴們看了代碼就會(huì)發(fā)現(xiàn)哪里有問(wèn)題了:更新鎖過(guò)期時(shí)間的代碼肯定不能這么去寫(xiě)。因?yàn)檫@么寫(xiě)會(huì) 導(dǎo)致當(dāng)前線程在更新鎖超時(shí)時(shí)間的while(true)循環(huán)中一直阻塞而無(wú)法返回結(jié)果。 所以,我們不能將當(dāng)前線程阻塞,需要異步執(zhí)行定時(shí)任務(wù)來(lái)更新鎖的過(guò)期時(shí)間。
此時(shí),我們繼續(xù)修改RedisLockImpl類的代碼,將定時(shí)更新鎖超時(shí)的代碼放到一個(gè)單獨(dú)的線程中執(zhí)行,如下所示。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
//如果獲取鎖失敗,則自旋獲取鎖,直到成功
if(!isLocked){
for(;;){
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
if(isLocked){
break;
}
}
}
//啟動(dòng)新線程來(lái)執(zhí)行定時(shí)任務(wù),更新鎖過(guò)期時(shí)間
new Thread(new UpdateLockTimeoutTask(uuid, stringRedisTemplate, key)).start();
}else{
isLocked = true;
}
//加鎖成功后將計(jì)數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
String uuid = stringRedisTemplate.opsForValue().get(key);
if(threadLocal.get().equals(uuid)){
Integer count = threadLocalInteger.get();
//計(jì)數(shù)器減為0時(shí)釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
//獲取更新鎖超時(shí)時(shí)間的線程并中斷
long threadId = stringRedisTemplate.opsForValue().get(uuid);
Thread updateLockTimeoutThread = ThreadUtils.getThreadByThreadId(threadId);
if(updateLockTimeoutThread != null){
//中斷更新鎖超時(shí)時(shí)間的線程
updateLockTimeoutThread.interrupt();
stringRedisTemplate.delete(uuid);
}
}
}
}
}
創(chuàng)建UpdateLockTimeoutTask類來(lái)執(zhí)行更新鎖超時(shí)的時(shí)間。
public class UpdateLockTimeoutTask implements Runnable{
//uuid
private long uuid;
private StringRedisTemplate stringRedisTemplate;
private String key;
public UpdateLockTimeoutTask(long uuid, StringRedisTemplate stringRedisTemplate, String key){
this.uuid = uuid;
this.stringRedisTemplate = stringRedisTemplate;
this.key = key;
}
@Override
public void run(){
//以u(píng)uid為key,當(dāng)前線程id為value保存到Redis中
stringRedisTemplate.opsForValue().set(uuid, Thread.currentThread().getId());
//定義更新鎖的過(guò)期時(shí)間
while(true){
springRedisTemplate.expire(key, 30, TimeUnit.SECONDS);
try{
//每隔10秒執(zhí)行一次
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
接下來(lái),我們定義一個(gè)ThreadUtils工具類,這個(gè)工具類中有一個(gè)根據(jù)線程id獲取線程的方法getThreadByThreadId(long threadId)。
public class ThreadUtils{
//根據(jù)線程id獲取線程句柄
public static Thread getThreadByThreadId(long threadId){
ThreadGroup group = Thread.currentThread().getThreadGroup();
while(group != null){
Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
int count = group.enumerate(threads, true);
for(int i = 0; i < count; i++){
if(threadId == threads[i].getId()){
return threads[i];
}
}
}
}
}
上述解決分布式鎖失效的問(wèn)題在分布式鎖領(lǐng)域有一個(gè)專業(yè)的術(shù)語(yǔ)叫做 “異步續(xù)命” 。需要注意的是:當(dāng)業(yè)務(wù)代碼執(zhí)行完畢后,我們需要停止更新鎖超時(shí)時(shí)間的線程。所以,這里,我對(duì)程序的改動(dòng)是比較大的,首先,將更新鎖超時(shí)的時(shí)間任務(wù)重新定義為一個(gè)UpdateLockTimeoutTask類,并將uuid和StringRedisTemplate注入到任務(wù)類中,在執(zhí)行定時(shí)更新鎖超時(shí)時(shí)間時(shí),首先將當(dāng)前線程保存到Redis中,其中Key為傳遞進(jìn)來(lái)的uuid。
在首先獲取分布式鎖后,重新啟動(dòng)線程,并將uuid和StringRedisTemplate傳遞到任務(wù)類中執(zhí)行任務(wù)。當(dāng)業(yè)務(wù)代碼執(zhí)行完畢后,調(diào)用releaseLock()方法釋放鎖時(shí),我們會(huì)通過(guò)uuid從Redis中獲取更新鎖超時(shí)時(shí)間的線程id,并通過(guò)線程id獲取到更新鎖超時(shí)時(shí)間的線程,調(diào)用線程的interrupt()方法來(lái)中斷線程。
此時(shí),當(dāng)分布式鎖釋放后,更新鎖超時(shí)的線程就會(huì)由于線程中斷而退出了。
實(shí)現(xiàn)分布式鎖的基本要求
結(jié)合上述的案例,我們可以得出實(shí)現(xiàn)分布式鎖的基本要求:
- 支持互斥性
- 支持鎖超時(shí)
- 支持阻塞和非阻塞特性
- 支持可重入性
- 支持高可用
通用分布式解決方案
在互聯(lián)網(wǎng)行業(yè),分布式鎖是一個(gè)繞不開(kāi)的話題,同時(shí),也有很多通用的分布式鎖解決方案,其中,用的比較多的一種方案就是使用開(kāi)源的Redisson框架來(lái)解決分布式鎖問(wèn)題。
有關(guān)Redisson分布式鎖的使用方案大家可以參考《【高并發(fā)】你知道嗎?大家都在使用Redisson實(shí)現(xiàn)分布式鎖了?。 ?/p>
既然Redisson框架已經(jīng)很牛逼了,我們直接使用Redisson框架是否能夠100%的保證分布式鎖不出問(wèn)題呢?答案是無(wú)法100%的保證。因?yàn)樵诜植际筋I(lǐng)域沒(méi)有哪一家公司或者架構(gòu)師能夠保證100%的不出問(wèn)題,就連阿里這樣的大公司、阿里的首席架構(gòu)師這樣的技術(shù)大牛也不敢保證100%的不出問(wèn)題。
在分布式領(lǐng)域,無(wú)法做到100%無(wú)故障,我們追求的是幾個(gè)9的目標(biāo),例如99.999%無(wú)故障。
CAP理論
在分布式領(lǐng)域,有一個(gè)非常重要的理論叫做CAP理論。
- C:Consistency(一致性)
- A:Availability(可用性)
- P:Partition tolerance(分區(qū)容錯(cuò)性)
在分布式領(lǐng)域中,是必須要保證分區(qū)容錯(cuò)性的,也就是必須要保證“P”,所以,我們只能保證CP或者AP。
這里,我們可以使用Redis和Zookeeper來(lái)進(jìn)行簡(jiǎn)單的對(duì)比,我們可以使用Redis實(shí)現(xiàn)AP架構(gòu)的分布式鎖,使用Zookeeper實(shí)現(xiàn)CP架構(gòu)的分布式鎖。
- 基于Redis的AP架構(gòu)的分布式鎖模型
在基于Redis實(shí)現(xiàn)的AP架構(gòu)的分布式鎖模型中,向Redis節(jié)點(diǎn)1寫(xiě)入數(shù)據(jù)后,會(huì)立即返回結(jié)果,之后在Redis中會(huì)以異步的方式來(lái)同步數(shù)據(jù)。
- 基于Zookeeper的CP架構(gòu)的分布式鎖模型
在基于Zookeeper實(shí)現(xiàn)的CP架構(gòu)的分布式模型中,向節(jié)點(diǎn)1寫(xiě)入數(shù)據(jù)后,會(huì)等待數(shù)據(jù)的同步結(jié)果,當(dāng)數(shù)據(jù)在大多數(shù)Zookeeper節(jié)點(diǎn)間同步成功后,才會(huì)返回結(jié)果數(shù)據(jù)。
當(dāng)我們使用基于Redis的AP架構(gòu)實(shí)現(xiàn)分布式鎖時(shí),需要注意一個(gè)問(wèn)題,這個(gè)問(wèn)題可以使用下圖來(lái)表示。
也就是Redis主從節(jié)點(diǎn)之間的數(shù)據(jù)同步失敗,假設(shè)線程向Master節(jié)點(diǎn)寫(xiě)入了數(shù)據(jù),而Redis中Master節(jié)點(diǎn)向Slave節(jié)點(diǎn)同步數(shù)據(jù)失敗了。此時(shí),另一個(gè)線程讀取的Slave節(jié)點(diǎn)中的數(shù)據(jù),發(fā)現(xiàn)沒(méi)有添加分布式鎖,此時(shí)就會(huì)出現(xiàn)問(wèn)題了?。?!
所以,在設(shè)計(jì)分布式鎖方案時(shí),也需要注意Redis節(jié)點(diǎn)之間的數(shù)據(jù)同步問(wèn)題。
紅鎖的實(shí)現(xiàn)
在Redisson框架中,實(shí)現(xiàn)了紅鎖的機(jī)制,Redisson的RedissonRedLock對(duì)象實(shí)現(xiàn)了Redlock介紹的加鎖算法。該對(duì)象也可以用來(lái)將多個(gè)RLock對(duì)象關(guān)聯(lián)為一個(gè)紅鎖,每個(gè)RLock對(duì)象實(shí)例可以來(lái)自于不同的Redisson實(shí)例。當(dāng)紅鎖中超過(guò)半數(shù)的RLock加鎖成功后,才會(huì)認(rèn)為加鎖是成功的,這就提高了分布式鎖的高可用。
我們可以使用Redisson框架來(lái)實(shí)現(xiàn)紅鎖。
public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 同時(shí)加鎖:lock1 lock2 lock3, 紅鎖在大部分節(jié)點(diǎn)上加鎖成功就算成功。
lock.lock();
// 嘗試加鎖,最多等待100秒,上鎖以后10秒自動(dòng)解鎖
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
其實(shí),在實(shí)際場(chǎng)景中,紅鎖是很少使用的。這是因?yàn)槭褂昧思t鎖后會(huì)影響高并發(fā)環(huán)境下的性能,使得程序的體驗(yàn)更差。所以,在實(shí)際場(chǎng)景中,我們一般都是要保證Redis集群的可靠性。
同時(shí),使用紅鎖后,當(dāng)加鎖成功的RLock個(gè)數(shù)不超過(guò)總數(shù)的一半時(shí),會(huì)返回加鎖失敗,即使在業(yè)務(wù)層面任務(wù)加鎖成功了,但是紅鎖也會(huì)返回加鎖失敗的結(jié)果。
另外,使用紅鎖時(shí),需要提供多套R(shí)edis的主從部署架構(gòu),同時(shí),這多套R(shí)edis主從架構(gòu)中的Master節(jié)點(diǎn)必須都是獨(dú)立的,相互之間沒(méi)有任何數(shù)據(jù)交互。
高并發(fā)“黑科技”與致勝奇招
假設(shè),我們就是使用Redis來(lái)實(shí)現(xiàn)分布式鎖,假設(shè)Redis的讀寫(xiě)并發(fā)量在5萬(wàn)左右。我們的商城業(yè)務(wù)需要支持的并發(fā)量在100萬(wàn)左右。
如果這100萬(wàn)的并發(fā)全部打入Redis中,Redis很可能就會(huì)掛掉,那么,我們?nèi)绾谓鉀Q這個(gè)問(wèn)題呢?接下來(lái),我們就一起來(lái)探討這個(gè)問(wèn)題。
在高并發(fā)的商城系統(tǒng)中,如果采用Redis緩存數(shù)據(jù),則Redis緩存的并發(fā)處理能力是關(guān)鍵,因?yàn)楹芏嗟那熬Y操作都需要訪問(wèn)Redis。而異步削峰只是基本的操作,關(guān)鍵還是要保證Redis的并發(fā)處理能力。
解決這個(gè)問(wèn)題的關(guān)鍵思想就是:分而治之,將商品庫(kù)存分開(kāi)放。
暗度陳倉(cāng)
我們?cè)赗edis中存儲(chǔ)商品的庫(kù)存數(shù)量時(shí),可以將商品的庫(kù)存進(jìn)行“分割”存儲(chǔ)來(lái)提升Redis的讀寫(xiě)并發(fā)量。
例如,原來(lái)的商品的id為10001,庫(kù)存為1000件,在Redis中的存儲(chǔ)為(10001, 1000),我們將原有的庫(kù)存分割為5份,則每份的庫(kù)存為200件,此時(shí),我們?cè)赗edia中存儲(chǔ)的信息為(10001_0, 200),(10001_1, 200),(10001_2, 200),(10001_3, 200),(10001_4, 200)。
此時(shí),我們將庫(kù)存進(jìn)行分割后,每個(gè)分割后的庫(kù)存使用商品id加上一個(gè)數(shù)字標(biāo)識(shí)來(lái)存儲(chǔ),這樣,在對(duì)存儲(chǔ)商品庫(kù)存的每個(gè)Key進(jìn)行Hash運(yùn)算時(shí),得出的Hash結(jié)果是不同的,這就說(shuō)明,存儲(chǔ)商品庫(kù)存的Key有很大概率不在Redis的同一個(gè)槽位中,這就能夠提升Redis處理請(qǐng)求的性能和并發(fā)量。
分割庫(kù)存后,我們還需要在Redis中存儲(chǔ)一份商品id和分割庫(kù)存后的Key的映射關(guān)系,此時(shí)映射關(guān)系的Key為商品的id,也就是10001,Value為分割庫(kù)存后存儲(chǔ)庫(kù)存信息的Key,也就是10001_0,10001_1,10001_2,10001_3,10001_4。在Redis中我們可以使用List來(lái)存儲(chǔ)這些值。
在真正處理庫(kù)存信息時(shí),我們可以先從Redis中查詢出商品對(duì)應(yīng)的分割庫(kù)存后的所有Key,同時(shí)使用AtomicLong來(lái)記錄當(dāng)前的請(qǐng)求數(shù)量,使用請(qǐng)求數(shù)量對(duì)從Redia中查詢出的商品對(duì)應(yīng)的分割庫(kù)存后的所有Key的長(zhǎng)度進(jìn)行求模運(yùn)算,得出的結(jié)果為0,1,2,3,4。再在前面拼接上商品id就可以得出真正的庫(kù)存緩存的Key。此時(shí),就可以根據(jù)這個(gè)Key直接到Redis中獲取相應(yīng)的庫(kù)存信息。
同時(shí),我們可以將分隔的不同的庫(kù)存數(shù)據(jù)分別存儲(chǔ)到不同的Redis服務(wù)器中,進(jìn)一步提升Redis的并發(fā)量。
移花接木
在高并發(fā)業(yè)務(wù)場(chǎng)景中,我們可以直接使用Lua腳本庫(kù)(OpenResty)從負(fù)載均衡層直接訪問(wèn)緩存。
這里,我們思考一個(gè)場(chǎng)景:如果在高并發(fā)業(yè)務(wù)場(chǎng)景中,商品被瞬間搶購(gòu)一空。此時(shí),用戶再發(fā)起請(qǐng)求時(shí),如果系統(tǒng)由負(fù)載均衡層請(qǐng)求應(yīng)用層的各個(gè)服務(wù),再由應(yīng)用層的各個(gè)服務(wù)訪問(wèn)緩存和數(shù)據(jù)庫(kù),其實(shí),本質(zhì)上已經(jīng)沒(méi)有任何意義了,因?yàn)樯唐芬呀?jīng)賣(mài)完了,再通過(guò)系統(tǒng)的應(yīng)用層進(jìn)行層層校驗(yàn)已經(jīng)沒(méi)有太多意義了!!而應(yīng)用層的并發(fā)訪問(wèn)量是以百為單位的,這又在一定程度上會(huì)降低系統(tǒng)的并發(fā)度。
為了解決這個(gè)問(wèn)題,此時(shí),我們可以在系統(tǒng)的負(fù)載均衡層取出用戶發(fā)送請(qǐng)求時(shí)攜帶的用戶id,商品id和活動(dòng)id等信息,直接通過(guò)Lua腳本等技術(shù)來(lái)訪問(wèn)緩存中的庫(kù)存信息。如果商品的庫(kù)存小于或者等于0,則直接返回用戶商品已售完的提示信息,而不用再經(jīng)過(guò)應(yīng)用層的層層校驗(yàn)了。