一,秒殺需要具備的功能:
秒殺通常是電商中用到的吸引流量的促銷活動方式
搭建秒殺系統,需要具備以下幾點:
1,限制每個用戶購買的商品數量,(秒殺價格為吸引流量一般會訂的很低,不能讓一個用戶全部搶購到手)
2,處理速度要快,避免在高並發的情況下發生堵塞
3,高並發情況下,不能出現庫存超賣的情況
因為redis中對lua腳本執行的原子性,不會出現因高並發而導致數據查詢的延遲
所以我們選擇使用redis+lua來實現秒殺的功能
例子:如果同一個秒殺活動中有多件商品,而有人用軟件刷接口的方式來下單,
這時就需要有針對當前活動的購買數量限制
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,本演示項目的相關信息
1,項目地址:
https://github.com/liuhongdi/seconddemo
2,項目原理:
在秒殺項目開始前,要把sku及其庫存數同步到redis中,
有秒殺請求時,判斷商品庫存數,
判斷用戶已購買的同一sku數量,
判斷用戶已購買的同一秒殺活動中的商品數量,
如果以上兩個數量大於0時,需要進行限制
如有問題時,返回秒殺失敗
都沒有問題時,減庫存,返回秒殺成功
要注意的地方:
秒殺前要獲取此活動中的對購買活動/sku的數量限制
秒殺成功后,如果用戶未支付導致訂單過期恢復庫存時,redis中的庫存數也要同步
3,項目結構:
三,lua代碼說明
1,second.lua
local userId = KEYS[1] local buyNum = tonumber(KEYS[2]) local skuId = KEYS[3] local perSkuLim = tonumber(KEYS[4]) local actId = KEYS[5] local perActLim = tonumber(KEYS[6]) local orderTime = KEYS[7] --用到的各個hash local user_sku_hash = 'sec_'..actId..'_u_sku_hash' local user_act_hash = 'sec_'..actId..'_u_act_hash' local sku_amount_hash = 'sec_'..actId..'_sku_amount_hash' local second_log_hash = 'sec_'..actId..'_log_hash' --當前sku是否還有庫存 local skuAmountStr = redis.call('hget',sku_amount_hash,skuId) if skuAmountStr == false then --redis.log(redis.LOG_NOTICE,'skuAmountStr is nil ') return '-3' end; local skuAmount = tonumber(skuAmountStr) --redis.log(redis.LOG_NOTICE,'sku:'..skuId..';skuAmount:'..skuAmount) if skuAmount <= 0 then return '0' end redis.log(redis.LOG_NOTICE,'perActLim:'..perActLim) local userActKey = userId..'_'..actId --當前用戶已購買此活動多少件 if perActLim > 0 then local userActNumInt = 0 local userActNum = redis.call('hget',user_act_hash,userActKey) if userActNum == false then --redis.log(redis.LOG_NOTICE,'userActKey:'..userActKey..' is nil') userActNumInt = buyNum else --redis.log(redis.LOG_NOTICE,userActKey..':userActNum:'..userActNum..';perActLim:'..perActLim) local curUserActNumInt = tonumber(userActNum) userActNumInt = curUserActNumInt+buyNum end if userActNumInt > perActLim then return '-2' end end local goodsUserKey = userId..'_'..skuId --redis.log(redis.LOG_NOTICE,'perSkuLim:'..perSkuLim) --當前用戶已購買此sku多少件 if perSkuLim > 0 then local goodsUserNum = redis.call('hget',user_sku_hash,goodsUserKey) local goodsUserNumint = 0 if goodsUserNum == false then --redis.log(redis.LOG_NOTICE,'goodsUserNum is nil') goodsUserNumint = buyNum else --redis.log(redis.LOG_NOTICE,'goodsUserNum:'..goodsUserNum..';perSkuLim:'..perSkuLim) local curSkuUserNumint = tonumber(goodsUserNum) goodsUserNumint = curSkuUserNumint+buyNum end --redis.log(redis.LOG_NOTICE,'------goodsUserNumint:'..goodsUserNumint..';perSkuLim:'..perSkuLim) if goodsUserNumint > perSkuLim then return '-1' end end --判斷是否還有庫存滿足當前秒殺數量 if skuAmount >= buyNum then local decrNum = 0-buyNum redis.call('hincrby',sku_amount_hash,skuId,decrNum) --redis.log(redis.LOG_NOTICE,'second success:'..skuId..'-'..buyNum) if perSkuLim > 0 then redis.call('hincrby',user_sku_hash,goodsUserKey,buyNum) end if perActLim > 0 then redis.call('hincrby',user_act_hash,userActKey,buyNum) end local orderKey = userId..'_'..skuId..'_'..buyNum..'_'..orderTime local orderStr = '1' redis.call('hset',second_log_hash,orderKey,orderStr) return orderKey else return '0' end
2,功能說明:
--用到的各個參數
local userId 用戶id
local buyNum 用戶購買的數量
local skuId 用戶購買的sku
local perSkuLim 每人購買此sku的數量限制
local actId 活動id
local perActLim 此活動中商品每人購買數量的限制
local orderTime 下訂單的時間
--用到的各個hash
local user_sku_hash 每個用戶購買的某一sku的數量
local user_act_hash 每個用戶購買的某一活動中商品的數量
local sku_amount_hash sku的庫存數
local second_log_hash 秒殺成功的記錄
判斷的流程:
判斷商品庫存數,
判斷用戶已購買的同一sku數量,
判斷用戶已購買的同一秒殺活動中的商品數量
四,java代碼說明:
1,SecondServiceImpl.java
功能:傳遞參數,執行秒殺功能
/* * 秒殺功能, * 調用second.lua腳本 * actId:活動id * userId:用戶id * buyNum:購買數量 * skuId:sku的id * perSkuLim:每個用戶購買當前sku的個數限制 * perActLim:每個用戶購買當前活動內所有sku的總數量限制 * 返回: * 秒殺的結果 * * */ @Override public String skuSecond(String actId,String userId,int buyNum,String skuId,int perSkuLim,int perActLim) { //時間字串,用來區分秒殺成功的訂單 int START = 100000; int END = 900000; int rand_num = ThreadLocalRandom.current().nextInt(END - START + 1) + START; String order_time = TimeUtil.getTimeNowStr()+"-"+rand_num; List<String> keyList = new ArrayList(); keyList.add(userId); keyList.add(String.valueOf(buyNum)); keyList.add(skuId); keyList.add(String.valueOf(perSkuLim)); keyList.add(actId); keyList.add(String.valueOf(perActLim)); keyList.add(order_time); String result = redisLuaUtil.runLuaScript("second.lua",keyList); System.out.println("------------------lua result:"+result); return result; }
2,RedisLuaUtil.java
功能:負責調用lua腳本的類
@Service public class RedisLuaUtil { @Resource private StringRedisTemplate stringRedisTemplate; private static final Logger logger = LogManager.getLogger("bussniesslog"); /* run a lua script luaFileName: lua file name,no path keyList: list for redis key return other: fail 1: success */ public String runLuaScript(String luaFileName,List<String> keyList) { DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName))); redisScript.setResultType(String.class); String result = ""; String argsone = "none"; //logger.error("開始執行lua"); try { result = stringRedisTemplate.execute(redisScript, keyList,argsone); } catch (Exception e) { logger.error("發生異常",e); } return result; } }
五,測試秒殺的效果
1,訪問:http://127.0.0.1:8080/second/index
添加庫存
如圖:
2,配置jmeter開始測試:
參見這一篇:
https://www.cnblogs.com/architectforest/p/13087798.html
定義測試用到的變量:
定義線程組數量為100
定義http請求:
在查看結果樹中查看結果:
3,查看代碼中的輸出:
------------------lua result:u3_cpugreen_1_20200611162435-487367 ------------------lua result:-2 ------------------lua result:u1_cpugreen_2_20200611162435-644085 ------------------lua result:u3_cpugreen_1_20200611162435-209653 ------------------lua result:-1 ------------------lua result:u2_cpugreen_1_20200611162434-333603 ------------------lua result:-1 ------------------lua result:-2 ------------------lua result:-1 ------------------lua result:u2_cpugreen_1_20200611162434-220636 ------------------lua result:-2 ------------------lua result:-1 ...
每個用戶的購買數量均未超過2單,秒殺的限制成功
六,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)