redis與Lua整合以及使用lua實現秒殺功能


一、安裝lua

  centos使用以下命令安裝

curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test
make install

  安裝過程中可能出現的異常及解決辦法如下:

  問題:

[root@liconglong-aliyun lua-5.3.0]# make linux test

......
make[2]: *** [lua.o] Error 1
make[2]: Leaving directory `/root/rj/lua/lua-5.3.0/src'
make[1]: *** [linux] Error 2
make[1]: Leaving directory `/root/rj/lua/lua-5.3.0/src'
make: *** [linux] Error 2

  解決方案:

yum install libtermcap-devel ncurses-devel libevent-devel readline-devel -y

二、Redis整合lua

  從redis2.6.0版本開始,通過內置的lua編譯器和解析器,可以使用eval命令對lua腳本進行求值

  eval命令:eval script numkeys key [key ...] arg[arg ...]

    其中script是一段lua腳本程序,numbers參數是指key參數個數,key參數和arg參數,分別可以使用KEYS[index]和ARGV[index]在script腳本中獲取,其中index從1開始

  示例如下:

127.0.0.1:6380> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 lcl qmm 20 18
1) "lcl"
2) "qmm"
3) "20"
4) "18"

三、lua腳本調用redis命令

1、redis.call()

  返回值就是redis命令執行的返回值,如果出錯,返回錯誤信息,不繼續執行

2、redis.pcall()

  返回值就是redis命令執行的返回值,如果出錯了,記錄錯誤信息,繼續執行

  在腳本中,使用return語句將返回值返回給客戶端,如果沒有return,則返回nil

127.0.0.1:6380> eval "return redis.call('set',KEYS[1],'testvalue')" 1 lclkey
OK
127.0.0.1:6380> get lclkey
"testvalue"

3、redis-cli --eval

   可以使用redis-cli --eval 命令指定一個lua腳本文件去執行。

--獲取指定值
local num = redis.call('get',KEYS[1]);
if not num then
 return num;
else
 local res = num * KEYS[2] * ARGV[1];
 redis.call('set',KEYS[1],res);
 return res;
end;

  設置key為luat的值為5(set luat 5)

  然后在linux中執行

[root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 , 30
(integer) 3000

  說明,這里要說明一下linux中執行lua腳本參數傳值和redis命令執行lua腳本傳值的差異問題,如果傳入多個參數,那么在redis命令中,需要指定key的個數,所有的key和argv參數之間都使用空格分隔即可,lua腳本執行時,會根據傳入的key個數自動區分開key參數和argv參數;但是在linux命令中,key參數和argv參數要用逗號分隔,key和key之間、argv與argv之間用空格分隔,如果key和argv之間不使用逗號,則會拋出異常,並且逗號前后需有空格,否則會被認為是傳的一個參數,同樣會拋出異常

  linux命令中參數傳值異常及正常傳值:

[root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 30
(error) ERR Error running script (call to f_8444ebd7385d71e3ee4daa6dc99acca626c75f4c): @user_script:6: user_script:6: attempt to perform arithmetic on field '?' (a nil value)
[root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20, 30
(error) ERR Error running script (call to f_8444ebd7385d71e3ee4daa6dc99acca626c75f4c): @user_script:6: user_script:6: attempt to perform arithmetic on field '?' (a string value)
[root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 , 30
(integer) 1800000

  redis中傳值可以參考第二點(Redis整合lua)

四、redis+lua秒殺

  先說一下秒殺需求,首先,現在有一個活動,活動ID為123,在這個活動中,可以秒殺或者搶購某件商品(假設商品的SKUID為369),一個用戶只允許搶購在該活動中搶購一件商品,同時,該用戶名下也最多只能有一件該商品(就是如果通過其他途徑買入,也不能參加該搶購活動)。

  了解了需求,那么首先就是編寫lua腳本,在項目的resource目錄下創建一個lua文件夾,專門放置lua腳本。

  根據需求,捋一下處理邏輯

    1、redis中要存sku的庫存總量,以防搶購超庫存量;要存用戶在該活動中已搶購數量,用來限制用戶在同一活動中搶購數量;要存用戶已購sku數量,用戶限制用戶購入sku總量;記錄一次活動中秒殺成功的次數,用以記錄

    2、校驗邏輯:庫存是否大於0;用戶搶購同一sku數量是否超過限制;用戶購買同一sku數量是否超過限制;本次搶購sku數量是否超過了庫存

    3、如果校驗通過,則搶購成功,sku庫存減一;用戶購買該sku的數量加一;用戶在該活動中搶購sku數量加一;用戶秒殺成功數加一

  腳本如下所示:里面已經加了注釋,不再一一說明代碼邏輯

--用戶Id
local userId = KEYS[1]
--用戶購買數量
local buynum = tonumber(KEYS[2])
--用戶購買的SKU
local skuid = KEYS[3]
--每人購買此SKU的數量限制
local perSkuLimit = tonumber(KEYS[4])
--活動Id
local actId = KEYS[5]
--此活動中商品每人購買限制
local perActLimit = tonumber(KEYS[6])
--訂單下單時間
local ordertime = KEYS[7]
--每個用戶購買的某一sku數量
local user_sku_hash = 'sec_'..actId..'_u_sku_hash'
--每個用戶購買的某一活動中商品的數量(已購買)
local user_act_hash = 'sec_'..actId..'_u_act_hash'
--sku的庫存數
local sku_amount_hash = 'sec_'..actId..'_sku_amount_hash'
--秒殺成功的記錄數
local second_log_hash = 'sec_'..actId..'_u_sku_hash'

--判斷的流程:

--判斷商品庫存數(當前sku是否還有庫存)
local skuAmountStr = redis.call('hget',sku_amount_hash,skuid) --獲取目前sku的庫存量
if skuAmountStr == false then  --如果沒有獲取到,則說明商品設置有誤,直接返回異常
    redis.log(redis.LOG_NOTICE,'skuAmountStr is nil ')
    return '-3'
end

local skuAmount = tonumber(skuAmountStr)
--如果庫存不大於0,則說明無庫存,不能再搶購
if skuAmount <= 0 then
    return '0'
end

local userActKey = userId..'_'..actId
--判斷用戶已購買的同一sku數量,

if perActLimit > 0 then  --如果每個人可以搶購的數量大於0,才能進行搶購,否則邏輯錯誤
    local userActNumInt = 0
    --獲取該活動中該用戶已經搶購到的數量
    local userActNum = redis.call('hget',user_act_hash,userActKey)
    --如果沒有獲取到,則說明用戶還未搶購到,直接搶購用戶下單的數量
    if userActNum == false then
        userActNumInt = buynum
    else   --如果獲取到了用戶在該活動中已經搶購到的數量,則用戶搶購成功后的sku總量=原有數量 + 本次下單數量
        local curUserActNumInt = tonumber(userActNum)
        userActNumInt = curUserActNumInt + buynum
    end

    --如果搶購成功后用戶在活動中搶購sku的數量大於每個用戶限制的數量,則返回異常
    if userActNumInt > perActLimit then
        return '-2'
    end
end

--判斷用戶已購買的同一秒殺活動中的商品數量
local goodsUserKey = userId..'_'..skuid
if perSkuLimit > 0 then --判斷每個用戶允許下單該sku的最大數量
    --獲取用戶已購買的sku數量
    local goodsUserNum = redis.call('hget',user_sku_hash,goodsUserKey)
    local goodsUserNumInt = 0
    --邏輯同上,如果獲取異常,說明用戶目前沒有購買過該sku,那么秒殺成功后購買sku的數量就是本次購買數量,否則就是本次購買數量 + 原有已購sku數量
    if goodsUserNum == false then
        goodsUserNumInt = buynum
    else
        local curSkuUserNumInt = tonumber(goodsUserNum)
        goodsUserNumInt = curSkuUserNumInt + buynum
    end
    --邏輯同上,如果本次購買成功后已購sku數量大於限制值,則返回異常
    if goodsUserNumInt > perSkuLimit then
        return '-1'
    end
end

--如果庫存數量大於秒殺數量,則將sku庫存減一;將用戶購買該sku的數量加一;將用戶在該活動中搶購sku數量加一;將用戶秒殺成功數加一;最終返回訂單號
if skuAmount >= buynum then
    local decrNum = 0-buynum
    -- sku庫存減一
    redis.call('hincrby',sku_amount_hash,skuid,decrNum)
    -- 用戶購買該sku的數量加一
    if perSkuLimit > 0 then
        redis.call('hincrby',user_sku_hash,goodsUserKey,buynum)
    end
    -- 用戶在該活動中搶購sku數量加一
    if perActLimit > 0 then
        redis.call('hincrby',user_act_hash,userActKey,buynum)
    end
    local orderKey = userId..'_'..'_'..buynum..'_'..ordertime
    local orderStr = '1'
    -- 用戶秒殺成功數加一
    redis.call('hset',second_log_hash,orderKey,orderStr)
    return orderKey
else
    return '0'
end

  然后就是對lua腳本的調用工具類

@Service
@Slf4j
public class RedisUtils {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    
    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) {
            log.error("秒殺失敗", e);
        }

        return result;
    }
}

  然后就是service層調用

@Service
public class LuaService {
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 秒殺功能,調用lua腳本
     *
     * @param actId     活動id
     * @param userId    用戶id
     * @param buyNum    購買數量
     * @param skuId     skuid
     * @param perSkuLim 每個用戶購買當前sku的數量限制
     * @param perActLim 每個用戶購買當前活動內所有sku的數量限制
     * @return
     */
    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 = getTime(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 = redisUtils.runLuaScript("order.lua", keyList);
        System.out.println("------------------lua result:" + result);
        return result;
    }

    private String getTime(int rand_num) {
        Date d = new Date();
        System.out.println(d);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateNowStr = sdf.format(d);
        return dateNowStr + "-" + rand_num;
    }
}

  Controller

@RestController
@RequestMapping("/redis")
public class RedisApi {

    @Autowired
    private LuaService luaService;

    @GetMapping("/lua")
    public String luaTest(String actId,String userId,int buyNum,String skuId,int perSkuLim,int perActLim){
        return luaService.skuSecond(actId,userId,buyNum,skuId,perSkuLim,perActLim);
    }
}

  在調用前,先在redis中設置skuid(369)的庫存,活動123中用戶147258369已搶購sku數量及用戶已購買數量

127.0.0.1:6388> hset sec_123_sku_amount_hash 369 10000
(integer) 0
127.0.0.1:6388> hset sec_123_u_act_hash 147258369_123 0
(integer) 0
127.0.0.1:6388> hset sec_123_u_sku_hash 147258369_369 0
(integer) 0

  然后調用:http://localhost:8080/redis/lua?actId=123&userId=147258369&buyNum=1&skuId=369&perSkuLim=1&perActLim=1

  就是在活動123中用戶147258369准備搶購1件skuid為369的商品,一個人最多在該活動中搶購一件369商品,一個人最多只能購買一件369商品

  

 

   調用成功,最后再查看響應的庫存等信息

127.0.0.1:6388> hget sec_123_sku_amount_hash 369
"9999"127.0.0.1:6388> hget sec_123_u_act_hash 147258369_123
"1"
127.0.0.1:6388> hget sec_123_u_sku_hash 147258369_369
"1"

  其實這里還可以模擬一下在高並發下進行調用,可以使用一些壓測工具進行測試,這里就不再說明。


免責聲明!

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



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