spring boot:redis+lua實現生產環境中可用的秒殺功能(spring boot 2.2.0)


一,秒殺需要具備的功能:

        秒殺通常是電商中用到的吸引流量的促銷活動方式

        搭建秒殺系統,需要具備以下幾點:

        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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM