Java秒殺系統(tǒng)(十四):基于Redis的原子操作優(yōu)化秒殺邏輯
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
摘要:本篇博文是“Java秒殺系統(tǒng)實(shí)戰(zhàn)系列文章”的第十四篇,本文將借助緩存中間件Redis的“單線程”特性及其原子操作一同優(yōu)化“秒殺系統(tǒng)中秒殺的核心業(yè)務(wù)邏輯”,徹底初步解決“庫(kù)存超賣”、“重復(fù)秒殺”等問(wèn)題。
內(nèi)容:對(duì)于緩存中間件Redis,相信各位小伙伴或多或少都有聽說(shuō)過(guò),甚至實(shí)戰(zhàn)過(guò),本文我們將基于SpringBoot整合Redis中間件,并基于其優(yōu)秀的“單線程”特性和原子操作實(shí)現(xiàn)一種“分布式鎖”,進(jìn)而控制“高并發(fā)情況下多線程對(duì)于共享資源的訪問(wèn)”,最終解決“并發(fā)安全”,即“庫(kù)存超賣”或者“重復(fù)秒殺”的問(wèn)題!
(1)按照慣例,首先我們需要加入Redis的第三方依賴,如下所示:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
緊接著,需要在application.properties配置文件中加入Redis服務(wù)所在的Host、端口Post、鏈接密鑰Password等信息,如下所示:
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
redis.config.host=redis://127.0.0.1:6379
(2)緊接著,我們還需要自定義注入跟Redis的操作組件相關(guān)的Bean配置,在這里主要是自定義注入配置RedisTemplate跟StringRedisTemplate操作組件,并指定其對(duì)應(yīng)的Key、Value的序列化策略:
// redis的通用化配置
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String,Object> redisTemplate(){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//TODO:指定Key、Value的序列化策略
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(){
StringRedisTemplate stringRedisTemplate=new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
(3)至此,可以說(shuō)是做好了充足的準(zhǔn)備,接下來(lái)我們就可以拿來(lái)用了!為了區(qū)分之前的秒殺邏輯方法,我們開了一個(gè)新的秒殺邏輯方法killItemV3,并采用Redis的原子操作SETNX和EXPIRE方法來(lái)實(shí)現(xiàn)一種“分布式鎖”,進(jìn)而控制高并發(fā)多線程對(duì)共享資源的訪問(wèn),其完整源代碼如下所示:
//商品秒殺核心業(yè)務(wù)邏輯的處理-redis的分布式鎖
@Override
public Boolean killItemV3(Integer killId, Integer userId) throws Exception {
Boolean result=false;
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
//TODO:借助Redis的原子操作實(shí)現(xiàn)分布式鎖-對(duì)共享操作-資源進(jìn)行控制
ValueOperations valueOperations=stringRedisTemplate.opsForValue();
final String key=new StringBuffer().append(killId).append(userId).append("-RedisLock").toString();
final String value=RandomUtil.generateOrderCode();
Boolean cacheRes=valueOperations.setIfAbsent(key,value);
if (cacheRes){
stringRedisTemplate.expire(key,30, TimeUnit.SECONDS);
try {
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
int res=itemKillMapper.updateKillItemV2(killId);
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);
result=true;
}
}
}catch (Exception e){
throw new Exception("還沒(méi)到搶購(gòu)日期、已過(guò)了搶購(gòu)時(shí)間或已被搶購(gòu)?fù)戤叄?);
}finally {
if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}
}
}
}else{
throw new Exception("Redis-您已經(jīng)搶購(gòu)過(guò)該商品了!");
}
return result;
}
在上述代碼中,我們主要是通過(guò)以下幾個(gè)操作綜合實(shí)現(xiàn)了“分布式鎖”的功能,其中包括
(1)valueOperations.setIfAbsent(key,value);:表示當(dāng)前的Key如果不存在于緩存中,那么將設(shè)置值成功,反之,如果Key已經(jīng)存在于緩存中了,那么設(shè)置值將不成功!通過(guò)這一特性,我們可以將“KillId和UserId的一一對(duì)應(yīng)關(guān)系~即一個(gè)人只能搶到一個(gè)商品”組合在一起作為Key!
(2)設(shè)置了Key,那么就需要在某個(gè)時(shí)間點(diǎn)去釋放,即stringRedisTemplate.expire(key,30, TimeUnit.SECONDS);操作可以輔助實(shí)現(xiàn)!
(3)然鵝,真正“釋放鎖”的操作是如下這段代碼去實(shí)現(xiàn)的,即判斷一下當(dāng)前要釋放的鎖是否就是之前一開始獲取到的鎖,如果是,就釋放!這一點(diǎn)可以很好的避免誤刪鎖的問(wèn)題!
if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}
至此,基于Redis的原子操作實(shí)現(xiàn)的分布式鎖,進(jìn)而控制高并發(fā)多線程對(duì)于共享資源的訪問(wèn),從而解決秒殺場(chǎng)景下“庫(kù)存超賣”、“重復(fù)秒殺”等問(wèn)題,下面采用JMeter進(jìn)行壓測(cè),壓測(cè)的用戶列表跟商品的“可秒殺數(shù)量total”跟上一篇章是一樣的,即total=6本書,用戶總共是10個(gè)。
點(diǎn)擊JMeter的啟動(dòng)按鈕,觀察控制臺(tái)的輸出信息以及數(shù)據(jù)庫(kù)表item_kill和item_kill_success表,可以看到秒殺記錄的結(jié)果很是令人滿意:
即庫(kù)存為6本的商品~書籍恰好被10個(gè)用戶中的6個(gè)秒殺得到!這種結(jié)果其實(shí)對(duì)于我們、對(duì)于用戶來(lái)講肯定是皆大歡喜的結(jié)局!
雖然演員對(duì)于自己的結(jié)局很滿意,但是導(dǎo)演卻察覺(jué)到戲中仍然有一些瑕疵!即如果秒殺系統(tǒng)在執(zhí)行Redis的原子操作SetNX后、執(zhí)行Expire之前,Redis的節(jié)點(diǎn)宕機(jī)了,那么此時(shí)將很有可能永久進(jìn)入“Key鎖死”的窘境,即重啟之后,由于之前的Key沒(méi)有得到釋放,故而這個(gè)Key將永遠(yuǎn)存在于緩存中,即對(duì)應(yīng)的用戶將不能秒殺該商品了!這一點(diǎn)確實(shí)是一個(gè)隱患!
既然存在隱患,那么我們就得想辦法解決了!莫急,下一篇章我們繼續(xù)!
補(bǔ)充:
1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實(shí)戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對(duì)應(yīng)的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點(diǎn)擊鏈接進(jìn)行試看以及學(xué)習(xí),實(shí)戰(zhàn)期間有任何問(wèn)題都可以留言或者與Debug聯(lián)系、交流!
2、另外,Debug也開源了該秒殺系統(tǒng)對(duì)應(yīng)的完整的源代碼以及數(shù)據(jù)庫(kù),其地址可以來(lái)這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star啊?。?!
3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號(hào):