redis lua腳本redis事務(wù)實(shí)現(xiàn) 商品秒殺活動(dòng)案例
1. 前言
redis 利用單線程 IO多路復(fù)用 實(shí)現(xiàn)了 單命令操作的原子性,但是多個(gè)命令的操作就不具備原子性赴背。
不過可以利用redis 事務(wù) 或者 lua腳本 來實(shí)現(xiàn) 多命令操作的原子性哨免。
本文試圖通過模擬商品秒殺活動(dòng),演示怎么實(shí)現(xiàn)redis多命令操作具有原子性假栓。
用到的工具: spring boot ,redis template,lua腳本邪锌。
2 準(zhǔn)備工作
2.1 配置redis Template
參照 Spring Boot 整合 RedisCache,EhCache,GuavaCache實(shí)戰(zhàn)中reids配置方式芒炼。
2.2 定義接口
public interface GoodsService {
/**
* 通過lua腳本實(shí)現(xiàn)的秒殺
* @param skuCode 商品編碼
* @param buyNum 購(gòu)買數(shù)量
* @return 購(gòu)買數(shù)量
*/
Long flashSellByLuaScript(String skuCode,int buyNum);
/**
* 通過redis 事務(wù) 實(shí)現(xiàn)的秒殺
* @param skuCode 商品編碼
* @param buyNum 購(gòu)買數(shù)量
* @return 購(gòu)買數(shù)量
*/
Long flashSellByRedisWatch(String skuCode,int buyNum);
}
3. redis 事務(wù)方式
- redisTemplate.excute(SessionCallback sessionCallback) 是執(zhí)行事務(wù)的api
- 所以要實(shí)現(xiàn)SessionCallback 來實(shí)現(xiàn)redis 事務(wù)批狐。
- 如果直接 通過redisTemplate 執(zhí)行事務(wù)命令 會(huì)報(bào)
redis.clients.jedis.exceptions.JedisDataException: Cannot use Jedis when in Multi. Please use Transation or reset jedis state
異常。
public class GoodsServiceImpl implements GoodsService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Long flashSellByRedisWatch(String skuCode,int num){
SessionCallback<Long> sessionCallback = new SessionCallback<Long>() {
@Override
public Long execute(RedisOperations operations) throws DataAccessException {
int result = num;
//redis 樂觀鎖
operations.watch(skuCode);
ValueOperations<String, String> valueOperations = operations.opsForValue();
String goodsNumStr = valueOperations.get(skuCode);
Integer goodsNum = Integer.valueOf(goodsNumStr);
//標(biāo)記一個(gè)事務(wù)塊的開始鸳谜。
//事務(wù)塊內(nèi)的多條命令會(huì)按照先后順序被放進(jìn)一個(gè)隊(duì)列當(dāng)中膝藕,
//最后由 EXEC 命令原子性(atomic)地執(zhí)行。
operations.multi();
if (goodsNum >= num) {
valueOperations.increment(skuCode, 0 - num);
} else {
result = 0;
}
//多條命令執(zhí)行的結(jié)果集合
List exec = operations.exec();
if(exec.size()>0){
System.out.println(exec);
}
return (long) result;
}
};
return stringRedisTemplate.execute(sessionCallback);
}
//省略 其他的方法
}
4. lua腳本方式
- 通過redis eval命令 執(zhí)行一個(gè)lua腳本
- 如果不明白 eval命令 閱讀 redis eval 命令詳解
4.1 編寫lua腳本
lua腳本是配置在 application.properties中的lua.flashSaleScript=
local buyNum = ARGV[1]
local goodsKey = KEYS[1]
local goodsNum = redis.call('get',goodsKey)
if goodsNum >= buyNum
then redis.call('decrby',goodsKey,buyNum)
return buyNum
else
return '0'
end
4.2 flashSellByLuaScript實(shí)現(xiàn)代碼
@Service
public class GoodsServiceImpl implements GoodsService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private LuaScript luaScript;
@Override
public Long flashSellByLuaScript(String skuCode,int num) {
DefaultRedisScript<String> longDefaultRedisScript = new DefaultRedisScript<>(luaScript.flashSaleScript, String.class);
String result = stringRedisTemplate.execute(longDefaultRedisScript, Collections.singletonList(skuCode),String.valueOf(num));
return Long.valueOf(result);
}
//省略 其他的方法
5. linux ab 壓力測(cè)試
- 提前在redis 中 存儲(chǔ) 貨品編碼為 0001 的貨品 咐扭,庫(kù)存數(shù)量為 500.
- 通過 linux ab 壓力測(cè)試 進(jìn)行測(cè)試 芭挽。
- 分別對(duì)兩種實(shí)現(xiàn)方式 進(jìn)行各1000次的訪問,并發(fā)數(shù)為 100蝗肪。
- 每次請(qǐng)求只購(gòu)買一個(gè)商品
5.1 ab命令
- shell
ab -n1000 -c100 -p data.json -T application/json http://192.168.33.222:8881/spring-boot/testFlashSell
- data.json
{
'skuCode':'0001',
'num':1
}
5.2 redis 事務(wù)方式執(zhí)行后
- 執(zhí)行一次 測(cè)試后袜爪,商品還剩余 369個(gè)。
- 執(zhí)行第二次后薛闪,商品還剩余 237個(gè)辛馆。
- 最后共執(zhí)行四次,就是共4000次請(qǐng)求后 商品數(shù)量才為0。
- 這個(gè)跟redis watch 的樂觀鎖有關(guān)昙篙。因?yàn)椴皇敲看握?qǐng)求都能成功腊状。
- 這種方式有可能會(huì)使本來可以賣完的商品 賣不完,或者需要更多的時(shí)間 才能售賣完苔可。
- 好在 并沒有出現(xiàn)負(fù)數(shù)缴挖。
192.168.26.230:14>get 0001
"0"
5.3 測(cè)試lua腳本方案
- 執(zhí)行一次后,商品就已經(jīng)為0.同樣也沒有出現(xiàn)負(fù)數(shù)焚辅。
6.總結(jié)
我比較喜歡lua腳本的實(shí)現(xiàn)方式映屋。對(duì)于程序開發(fā)者而言, 學(xué)習(xí)lua腳本很簡(jiǎn)單同蜻,稍微看下就可以編寫用于redis 的lua腳本棚点。