眾所周知,分布式鎖在微服務(wù)架構(gòu)中是重頭戲,尤其是在互聯(lián)網(wǎng)公司,基本上企業(yè)內(nèi)部都會(huì)有自己的一套分布式鎖開(kāi)發(fā)框架。本文主要介紹使用redis如何構(gòu)建高并發(fā)分布式鎖。
成都創(chuàng)新互聯(lián)從2013年創(chuàng)立,是專(zhuān)業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站建設(shè)、網(wǎng)站制作網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元保康做網(wǎng)站,已為上家服務(wù),為??蹈鞯仄髽I(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108
假設(shè) 存在一個(gè)SpringBoot的控制器,其扣減庫(kù)存的業(yè)務(wù)邏輯如下:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
// 將庫(kù)存取出來(lái)
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
return "finished.";
}
不難看出,在應(yīng)用服務(wù)器運(yùn)行這段代碼的時(shí)候就會(huì)有線程安全性問(wèn)題。因?yàn)槎鄠€(gè)線程同時(shí)去修改Redis服務(wù)中的數(shù)據(jù)。因此考慮給這段代碼加上一把鎖:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
synchronized (this) {
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
}
return "finished.";
}
這樣一來(lái),當(dāng)多個(gè)HTTP請(qǐng)求來(lái)請(qǐng)求數(shù)據(jù)的時(shí)候,多個(gè)線程去修改同一數(shù)據(jù)會(huì)有JVM本地鎖來(lái)進(jìn)行合理的資源限制。雖然這樣解決了線程安全性問(wèn)題,但是這僅僅是JVM級(jí)別的鎖,在分布式的環(huán)境下,由于像這樣的Web應(yīng)用隨時(shí)會(huì)進(jìn)行動(dòng)態(tài)擴(kuò)容,因此當(dāng)多個(gè)應(yīng)用的時(shí)候,同樣會(huì)有線程安全性問(wèn)題,當(dāng)上面這段代碼遇到類(lèi)似下面的架構(gòu)時(shí)還是會(huì)有各種各樣的問(wèn)題:
對(duì)于上述的情況,我們可以使用redis api提供的setnx方法解決:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
// 嘗試獲取鎖
Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");
// 判斷是否獲得鎖
if (!flag) { return "error"; }
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
// 刪除鎖
stringRedisTemplate.delete("Hello");
return "finished.";
}
setnx key value
是將key的值設(shè)置為value,當(dāng)且僅當(dāng)key不存在的時(shí)候。如果設(shè)置成功就返回1,否則就返回0。
這樣的話,首先嘗試獲取鎖,然后當(dāng)業(yè)務(wù)執(zhí)行完成的時(shí)候再刪除鎖。但是還是有問(wèn)題的,當(dāng)獲取鎖的時(shí)候拋出異?;蛘邩I(yè)務(wù)執(zhí)行拋出異常怎么辦,所以加入異常處理邏輯:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
try {
// 嘗試獲取鎖
Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");
// 判斷是否獲得鎖
if (!flag) { return "error"; }
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
} finally {
// 刪除鎖
stringRedisTemplate.delete("Hello");
}
return "finished.";
}
經(jīng)過(guò)這樣的修改,看起來(lái)沒(méi)什么問(wèn)題了。但是當(dāng)程序獲得鎖并且開(kāi)始執(zhí)行業(yè)務(wù)邏輯的時(shí)候,突然程序掛掉了或者被一些粗暴的運(yùn)維工程師給kill,在finally中刪除鎖的邏輯就會(huì)得不到執(zhí)行,因此就會(huì)產(chǎn)生死鎖。對(duì)于這種情況,我們可以給這個(gè)鎖設(shè)置一個(gè)超時(shí)時(shí)間:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
try {
// 嘗試獲取鎖
Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World");
// 設(shè)置超時(shí)時(shí)間, 根據(jù)業(yè)務(wù)場(chǎng)景估計(jì)超時(shí)時(shí)長(zhǎng)
stringRedisTmplate.expire("Hello", 10, TimeUnit.SECONDS);
// 判斷是否獲得鎖
if (!flag) { return "error"; }
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
} finally {
// 刪除鎖
stringRedisTemplate.delete("Hello");
}
return "finished.";
}
如果程序這么來(lái)寫(xiě),相對(duì)來(lái)說(shuō)安全一些了,但是還是存在問(wèn)題。試想一下,當(dāng)獲取鎖成功時(shí),正想給這把鎖設(shè)置超時(shí)的時(shí)候,程序掛掉了,還是會(huì)出現(xiàn)死鎖的,因此在redis較高的版本中提供的setIfAbsent方法中可以同時(shí)設(shè)置鎖的超時(shí)時(shí)間。
Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", "World", 10, TimeUnit.SECONDS);
這樣一來(lái),嘗試獲取鎖和設(shè)置鎖的超時(shí)時(shí)間就具備原子性了。實(shí)際上經(jīng)過(guò)我們這一番改造,這在小型企業(yè)已經(jīng)沒(méi)有太大的問(wèn)題, 因?yàn)橄襁@種代碼每天也就執(zhí)行幾百次,并不算做高并發(fā)的場(chǎng)景。當(dāng)這樣的代碼被暴露在超高并發(fā)場(chǎng)景下的時(shí)候,還是會(huì)存在各種各樣的問(wèn)題。試想一個(gè)場(chǎng)景,當(dāng)一個(gè)HTTP請(qǐng)求請(qǐng)求到控制器的時(shí)候,應(yīng)用獲取到鎖了,超時(shí)時(shí)間也設(shè)置成功了,但是應(yīng)用的業(yè)務(wù)邏輯超過(guò)了超時(shí)時(shí)間,我們這里的超時(shí)時(shí)間設(shè)置的是10秒,當(dāng)應(yīng)用的業(yè)務(wù)邏輯執(zhí)行15秒的時(shí)候,鎖就被redis服務(wù)刪除了。假設(shè)恰好此時(shí)又有一個(gè)HTTP請(qǐng)求來(lái)請(qǐng)求控制器,此時(shí)應(yīng)用服務(wù)器會(huì)再啟動(dòng)一個(gè)線程來(lái)獲取鎖,而且還獲取成功了,但是這次的HTTP請(qǐng)求對(duì)應(yīng)的業(yè)務(wù)邏輯還沒(méi)有執(zhí)行完。新來(lái)的TTTP請(qǐng)求也在執(zhí)行,由于新來(lái)的HTTP請(qǐng)求也在執(zhí)行,因?yàn)殒i超時(shí)后被刪除,新的HTTP請(qǐng)求也成功獲取鎖了。當(dāng)原來(lái)的HTTP請(qǐng)求對(duì)應(yīng)的業(yè)務(wù)邏輯執(zhí)行完成以后,嘗試刪除鎖,這樣正好刪除的是新來(lái)的HTTP請(qǐng)求對(duì)應(yīng)的鎖。這個(gè)時(shí)候redis中又沒(méi)有鎖了,這樣第三個(gè)HTTP請(qǐng)求又會(huì)獲得鎖,所以情況就不妙了。
為了解決上面的問(wèn)題,我們可以將代碼優(yōu)化為下面的樣子:
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
String clientUuid = UUID.randomUUID().toString();
try {
// 嘗試獲取鎖,設(shè)置超時(shí)時(shí)間, 根據(jù)業(yè)務(wù)場(chǎng)景估計(jì)超時(shí)時(shí)長(zhǎng)
Boolean flag = stringRedisTmplate.opsForValue().setIfAbsent("Hello", clientUuid, 10, TimeUnit.SECONDS);
// 判斷是否獲得鎖
if (!flag) { return "error"; }
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
} finally {
// 刪除鎖的時(shí)候判斷是不是自己的鎖
if (clientUuid.equals(stringRedisTemplate.opsForValue().get("Hello"))) {
stringRedisTemplate.delete("Hello");
}
}
return "finished.";
}
但是由于程序的不可預(yù)知性,誰(shuí)也不能保證極端情況下,同時(shí)會(huì)有多個(gè)線程同時(shí)執(zhí)行這段業(yè)務(wù)邏輯。我們可以在當(dāng)執(zhí)行業(yè)務(wù)邏輯的時(shí)候同時(shí)開(kāi)一個(gè)定時(shí)器線程,每隔幾秒就重新將這把鎖設(shè)置為10秒,也就是給這把鎖進(jìn)行“續(xù)命”。這樣就用擔(dān)心業(yè)務(wù)邏輯到底執(zhí)行多長(zhǎng)時(shí)間了。但是這樣程序的復(fù)雜性就會(huì)增加,每個(gè)業(yè)務(wù)邏輯都要寫(xiě)好多的代碼,因此這里推薦在分布式環(huán)境下使用redisson。因此我們使用redisson實(shí)現(xiàn)分支線程的代碼:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
@Bean
public Redisson redisson () {
Config cfg = new Config();
cfg.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(cfg);
}
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping(value = "/deduct-stock")
public String deductSotck() throws Exception {
// 獲取鎖對(duì)象
RLock lock = redisson.getLock("Hello");
try {
// 嘗試加鎖, 默認(rèn)30秒, 自動(dòng)后臺(tái)開(kāi)一個(gè)線程實(shí)現(xiàn)鎖的續(xù)命
lock.tryLock();
int i = Interger.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 判斷庫(kù)存夠不夠減
if (stock > 0) {
// 將庫(kù)存回寫(xiě)到redis
int tmp = stock - 1;
stringRedisTemplate.opsForValue().set("stock", tmp.toString());
logger.info("庫(kù)存扣減成功");
} else {
logger.info("庫(kù)存扣減失敗");
}
} finally {
// 釋放鎖
lock.unlock();
}
return "finished.";
}
Redisson分布式鎖的實(shí)現(xiàn)原理如下:
但是這個(gè)架構(gòu)還是存在問(wèn)題的,因?yàn)閞edis服務(wù)器是主從的架構(gòu),當(dāng)在master節(jié)點(diǎn)設(shè)置鎖之后,slave節(jié)點(diǎn)會(huì)立刻同步。但是如果剛在master節(jié)點(diǎn)設(shè)置上了鎖,slave節(jié)點(diǎn)還沒(méi)來(lái)得及設(shè)置,master節(jié)點(diǎn)就掛掉了。還是會(huì)產(chǎn)生上同樣的問(wèn)題,新的線程獲得鎖。
因此使用redis構(gòu)建高并發(fā)的分布式鎖,僅適合單機(jī)架構(gòu),當(dāng)使用主從架構(gòu)的redis時(shí)還是會(huì)出現(xiàn)線程安全性問(wèn)題。
本文名稱(chēng):Redis如何實(shí)現(xiàn)高并發(fā)分布式鎖?
文章位置:http://jinyejixie.com/article10/pggcgo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供軟件開(kāi)發(fā)、域名注冊(cè)、用戶體驗(yàn)、網(wǎng)站內(nèi)鏈、定制開(kāi)發(fā)、
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)