spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)
一,秒杀需要具备的功能:
秒杀通常是电商中用到的吸引流量的促销活动方式
搭建秒杀系统,需要具备以下几点:
1,限制每个用户购买的商品数量,(秒杀价格为吸引流量一般会订的很低,不能让一个用户全部抢购到手)
2,处理速度要快,避免在高并发的情况下发生堵塞
3,高并发情况下,不能出现库存超卖的情况
因为redis中对lua脚本执行的原子性,不会出现因高并发而导致数据查询的延迟
所以我们选择使用redis+lua来实现秒杀的功能
例子:如果同一个秒杀活动中有多件商品,而有人用软件刷接口的方式来下单,
这时就需要有针对当前活动的购买数量限制
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/22/springbootredislua-shi-xian-sheng-chan-huan-jing-zhong-ke-yong-de-miao-sha-gong-neng-springboot220/
对应的源码可以访问这里获取: 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开始测试:
参见这一篇:
定义测试用到的变量:
定义线程组数量为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)