一,秒杀需要具备的功能:
秒杀通常是电商中用到的吸引流量的促销活动方式
搭建秒杀系统,需要具备以下几点:
1,限制每个用户购买的商品数量,(秒杀价格为吸引流量一般会订的很低,不能让一个用户全部抢购到手)
2,处理速度要快,避免在高并发的情况下发生堵塞
3,高并发情况下,不能出现库存超卖的情况
因为redis中对lua脚本执行的原子性,不会出现因高并发而导致数据查询的延迟
所以我们选择使用redis+lua来实现秒杀的功能
例子:如果同一个秒杀活动中有多件商品,而有人用软件刷接口的方式来下单,
这时就需要有针对当前活动的购买数量限制
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔
二,本演示项目的相关信息
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)