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
就是在活动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"
其实这里还可以模拟一下在高并发下进行调用,可以使用一些压测工具进行测试,这里就不再说明。
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~