1.簡介
通過模擬搶購商品的實(shí)踐闡述高并發(fā)與鎖的問題临梗。這里假設(shè)電商網(wǎng)站搶購的場景假勿,電商網(wǎng)站往往存在很多的商品呜呐,有些商品會(huì)以低價(jià)限量推銷灾票,并且會(huì)在推銷之前做廣告以吸引網(wǎng)站會(huì)員購買寸宵。特別是熱銷產(chǎn)品巩检,很有可能會(huì)出現(xiàn)瞬時(shí)高并發(fā)的搶購患久,也就是我們常說的“商品秒殺”椅寺。這種情況在工作中很是常見,而且在面試的時(shí)候往往也是一個(gè)熱點(diǎn)考察的問題蒋失,下面就由我來給大家講解下如何處理這類高并發(fā)問題返帕。
2.項(xiàng)目準(zhǔn)備
首先,我先在redis中放入stock和商品數(shù)量篙挽,數(shù)量為100荆萤。
然后再通過jmeter來模擬高并發(fā)場景。
3.項(xiàng)目實(shí)踐
3.1 單機(jī)服務(wù)版本代碼
首先铣卡,我們先來看一個(gè)基礎(chǔ)的版本链韭。
@RequestMapping("/getStock")
public String getStock(){
//加鎖
synchronized (this){
//從redis中取庫存
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
//判斷剩余庫存
if(stock>0){
//如果還有庫存,則購買
int newStock = stock-1;
//再放回redis
redisTemplate.opsForValue().set(STOCK,String.valueOf(newStock));
System.out.println("扣減成功煮落,當(dāng)前庫存為:"+newStock);
}else {
System.out.println("扣減【失敗】梧油,當(dāng)前庫存為:"+stock);
}
return "success";
}
扣減成功,當(dāng)前庫存為:99
扣減成功州邢,當(dāng)前庫存為:98
扣減成功儡陨,當(dāng)前庫存為:97
………………
扣減成功,當(dāng)前庫存為:9
扣減成功量淌,當(dāng)前庫存為:8
扣減成功骗村,當(dāng)前庫存為:7
扣減成功,當(dāng)前庫存為:6
扣減成功呀枢,當(dāng)前庫存為:5
扣減成功胚股,當(dāng)前庫存為:4
扣減成功,當(dāng)前庫存為:3
扣減成功裙秋,當(dāng)前庫存為:2
扣減成功琅拌,當(dāng)前庫存為:1
扣減成功缨伊,當(dāng)前庫存為:0
扣減【失敗】,當(dāng)前庫存為:0
扣減【失敗】进宝,當(dāng)前庫存為:0
扣減【失敗】刻坊,當(dāng)前庫存為:0
扣減【失敗】,當(dāng)前庫存為:0
扣減【失敗】党晋,當(dāng)前庫存為:0
…………
從結(jié)果看來谭胚,這樣的代碼在單機(jī)環(huán)境下沒有什么問題
但如果是分布式部署,那這樣就會(huì)出現(xiàn)超賣現(xiàn)象未玻!
很顯然灾而,上面的代碼并不滿足分布式環(huán)境下的系統(tǒng)需求,下面我們將使用分布式鎖來改進(jìn)這個(gè)問題扳剿。
3.2 分布式服務(wù)版本代碼
在這個(gè)2.0的版本里旁趟,我們使用redis中的setnx數(shù)據(jù)結(jié)構(gòu)來做分布式鎖。
private static String LOCK_KEY = "lockKey";
@RequestMapping("/getStock2")
public String getStock2() {
//如果這里不設(shè)置過期時(shí)間庇绽,很可能會(huì)出現(xiàn)問題锡搜,例如其中一臺(tái)機(jī)器未釋放鎖便出現(xiàn)異常或宕機(jī)敛劝,那后面的請求都無法購買商品
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "lock", 10, TimeUnit.SECONDS);
if (!result) {
return "活動(dòng)太火爆了,請稍后重試";
}
try {
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
if (stock > 0) {
int newStock = stock - 1;
redisTemplate.opsForValue().set(STOCK, String.valueOf(newStock));
System.out.println("扣減成功纷宇,扣減后庫存為:" + newStock);
} else {
System.out.println("扣減【失敗】夸盟,當(dāng)前庫存為:" + stock);
}
} finally {
//此處要盡量保證鎖的釋放
redisTemplate.delete(LOCK_KEY);
}
return "success";
}
上面這個(gè)代碼,在并發(fā)不太高的情況下像捶,基本可以使用上陕,然而在高并發(fā)情況下依然存在鎖失效問題。
解決這個(gè)問題的關(guān)鍵是拓春,我自己加的鎖應(yīng)該只能由我自己釋放释簿。
3.3 使用redisson實(shí)現(xiàn)分布式鎖
要實(shí)現(xiàn)一個(gè)好的分布式鎖,應(yīng)包含以下功能:
- 指定一個(gè) key 作為鎖標(biāo)記硼莽,存入 Redis 中庶溶,指定一個(gè) 唯一的用戶標(biāo)識 作為 value。
- 當(dāng) key 不存在時(shí)才能設(shè)置值懂鸵,確保同一時(shí)間只有一個(gè)客戶端進(jìn)程獲得鎖偏螺,滿足 互斥性 特性。
- 設(shè)置一個(gè)過期時(shí)間匆光,防止因系統(tǒng)異常導(dǎo)致沒能刪除這個(gè) key套像,滿足 防死鎖 特性。
- 當(dāng)處理完業(yè)務(wù)之后需要清除這個(gè) key 來釋放鎖终息,清除 key 時(shí)需要校驗(yàn) value 值夺巩,需要滿足 只有加鎖的人才能釋放鎖 贞让。
這些我們都可以通過redisson來輕松的使用分布式鎖,代碼如下:
@RequestMapping("/getStock3")
public String getStock3() {
RLock rLock = redisson.getLock(LOCK_KEY);
try {
rLock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
if (stock > 0) {
int newStock = stock - 1;
redisTemplate.opsForValue().set(STOCK, String.valueOf(newStock));
System.out.println("扣減成功柳譬,扣減后庫存為:" + newStock);
} else {
System.out.println("扣減【失敗】喳张,當(dāng)前庫存為:" + stock);
}
} finally {
rLock.unlock();
}
return "success";
}
在大部分情況下,我們都可以使用redisson來完成我們的分布式鎖征绎,來應(yīng)對高并發(fā)的問題蹲姐,