Java秒殺系統(tǒng)(十四):基于Redis的原子操作優(yōu)化秒殺邏輯

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。



摘要:本篇博文是“Java秒殺系統(tǒng)實戰(zhàn)系列文章”的第十四篇,本文將借助緩存中間件Redis的“單線程”特性及其原子操作一同優(yōu)化“秒殺系統(tǒng)中秒殺的核心業(yè)務邏輯”,徹底初步解決“庫存超賣”、“重復秒殺”等問題。

內(nèi)容:對于緩存中間件Redis,相信各位小伙伴或多或少都有聽說過,甚至實戰(zhàn)過,本文我們將基于SpringBoot整合Redis中間件,并基于其優(yōu)秀的“單線程”特性和原子操作實現(xiàn)一種“分布式鎖”,進而控制“高并發(fā)情況下多線程對于共享資源的訪問”,最終解決“并發(fā)安全”,即“庫存超賣”或者“重復秒殺”的問題!

(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服務所在的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操作組件,并指定其對應的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)至此,可以說是做好了充足的準備,接下來我們就可以拿來用了!為了區(qū)分之前的秒殺邏輯方法,我們開了一個新的秒殺邏輯方法killItemV3,并采用Redis的原子操作SETNX和EXPIRE方法來實現(xiàn)一種“分布式鎖”,進而控制高并發(fā)多線程對共享資源的訪問,其完整源代碼如下所示:  

//商品秒殺核心業(yè)務邏輯的處理-redis的分布式鎖
@Override
public Boolean killItemV3(Integer killId, Integer userId) throws Exception {
Boolean result=false;

if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){

//TODO:借助Redis的原子操作實現(xià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("還沒到搶購日期、已過了搶購時間或已被搶購完畢!");
}finally {
if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}
}
}
}else{
throw new Exception("Redis-您已經(jīng)搶購過該商品了!");
}
return result;
}

在上述代碼中,我們主要是通過以下幾個操作綜合實現(xiàn)了“分布式鎖”的功能,其中包括

(1)valueOperations.setIfAbsent(key,value);:表示當前的Key如果不存在于緩存中,那么將設置值成功,反之,如果Key已經(jīng)存在于緩存中了,那么設置值將不成功!通過這一特性,我們可以將“KillId和UserId的一一對應關(guān)系~即一個人只能搶到一個商品”組合在一起作為Key!

(2)設置了Key,那么就需要在某個時間點去釋放,即stringRedisTemplate.expire(key,30, TimeUnit.SECONDS);操作可以輔助實現(xiàn)!

(3)然鵝,真正“釋放鎖”的操作是如下這段代碼去實現(xiàn)的,即判斷一下當前要釋放的鎖是否就是之前一開始獲取到的鎖,如果是,就釋放!這一點可以很好的避免誤刪鎖的問題!

if (value.equals(valueOperations.get(key).toString())){
stringRedisTemplate.delete(key);
}

至此,基于Redis的原子操作實現(xiàn)的分布式鎖,進而控制高并發(fā)多線程對于共享資源的訪問,從而解決秒殺場景下“庫存超賣”、“重復秒殺”等問題,下面采用JMeter進行壓測,壓測的用戶列表跟商品的“可秒殺數(shù)量total”跟上一篇章是一樣的,即total=6本書,用戶總共是10個。

點擊JMeter的啟動按鈕,觀察控制臺的輸出信息以及數(shù)據(jù)庫表item_kill和item_kill_success表,可以看到秒殺記錄的結(jié)果很是令人滿意:


即庫存為6本的商品~書籍恰好被10個用戶中的6個秒殺得到!這種結(jié)果其實對于我們、對于用戶來講肯定是皆大歡喜的結(jié)局!

雖然演員對于自己的結(jié)局很滿意,但是導演卻察覺到戲中仍然有一些瑕疵!即如果秒殺系統(tǒng)在執(zhí)行Redis的原子操作SetNX后、執(zhí)行Expire之前,Redis的節(jié)點宕機了,那么此時將很有可能永久進入“Key鎖死”的窘境,即重啟之后,由于之前的Key沒有得到釋放,故而這個Key將永遠存在于緩存中,即對應的用戶將不能秒殺該商品了!這一點確實是一個隱患!

既然存在隱患,那么我們就得想辦法解決了!莫急,下一篇章我們繼續(xù)!

補充:

1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對應的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點擊鏈接進行試看以及學習,實戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!

2、另外,Debug也開源了該秒殺系統(tǒng)對應的完整的源代碼以及數(shù)據(jù)庫,其地址可以來這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star啊?。?!

3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號: