一、安裝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
就是在活動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"
其實這里還可以模擬一下在高並發下進行調用,可以使用一些壓測工具進行測試,這里就不再說明。