概要
提示:这里可以添加技术概要
例如:
在用户基数越来越大的情况下,系统通常都会遇到高并发情况,,这对系统的设计提出了更高的要求。本次示例,以抢购优惠券为例子,来探究单机情况下的QPS。
整体架构流程
提示:这里可以添加技术整体架构
整体流程如下:
1.抢购之前,系统启动后,把优惠券信息加载到缓存中。
2.开始抢购
3.异步处理抢购订单
技术细节
1.缓存预热:
这里,采用来完成优惠券缓存预热,实际生产中可根据需求自行选择各种方式来完成此步骤
提示:这里因为数据库中只放了一条优惠券数据,所以采用了查询所有的方法
@Component
public class CouponInitRunner implements ApplicationRunner {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private CouponInfoService couponInfoService;
@Override
public void run(ApplicationArguments args) throws Exception {
//容器初始化之后,把优惠券信息存入到redis中
List<CouponInfo> list = couponInfoService.findAll();
for (int i = 0; i <list.size() ; i++) {
CouponInfo couponInfo = list.get(i);
stringRedisTemplate.opsForValue().set("seckill:coupon:stock:" + couponInfo.getId(), couponInfo.getStock().toString());
}
}
}
2.抢购流程
3.关键代码如下:
3.1 抢购代码
@Service
public class CouponSeckillServiceImpl implements ICouponSeckillService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final DefaultRedisScript<Long> SECKILL_LUA;
@Autowired(required = false)
private IdCreaterUtils idCreater;
@Autowired
private RabbitTemplate rabbitTemplate;
static {
SECKILL_LUA = new DefaultRedisScript<>();
SECKILL_LUA.setLocation(new ClassPathResource("seckillCoupon.lua"));
SECKILL_LUA.setResultType(Long.class);
}
/**
* 优惠券秒杀
* @param couponId
* @return
*/
@Override
public Result seckillCoupon(String userId,Long couponId) {
//1.首先判断用户信息和优惠券信息。比如用户id是否为空,券id是否正确,是否到了券的抢购时间
if (StringUtils.isBlank(userId)){
return new Result(false, StatusCode.ERROR, "用户信息错误");
}
//2.业务幂等性。判断该用户是否已经抢购过了
//3.抢购优惠券。抢到优惠券
//4.扣减优惠券库存。抢到优惠券之后,就要把优惠券的库存-1
//第2步和第3步以及第4步骤,为了防止超卖以及出现多次抢券的问题,采用lua脚本保证数据的原子性
long seckilledOrderId = idCreater.nextId();//通过雪花算法的ID生成器生成秒杀到的订单id
Long result = stringRedisTemplate.execute(
SECKILL_LUA,//lua脚本
Collections.emptyList(),//key。因为key我们采用在lua脚本中定义了,这里不用传
couponId.toString(), userId.toString(), String.valueOf(seckilledOrderId) //传递给lua脚本的参数
);
int seckill_resultVal = result.intValue();
// 2.判断结果是否为0
if (seckill_resultVal != 0) {
// 不为0 ,代表没有购买资格
if(seckill_resultVal==1){
new Result(false, StatusCode.ERROR, "库存不足");
}else{
new Result(false, StatusCode.ERROR, "不能重复抢购");
}
}
//5.抢购成功,生成订单信息,用mq发送消息,异步处理
SeckilledOrderVO orderVO=new SeckilledOrderVO(seckilledOrderId,couponId,userId);
rabbitTemplate.convertAndSend("seckillFanoutExchange", JSON.toJSONString(orderVO));
//6.返回信息给用户,也就是订单id
return new Result(true, StatusCode.OK, String.valueOf(seckilledOrderId));
}
}
3.2 lua脚本如下
-- 1.参数列表
local couponId = ARGV[1]
local userId = ARGV[2]
-- 2.数据key
local stockKey = 'seckill:coupon:stock:' .. couponId
local orderKey = 'seckill:coupon:order:' .. couponId
-- 3.脚本业务
-- 判断库存是否充足
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 库存不足,返回1
return 1
end
-- 4.判断用户是否下单
if(redis.call('sismember', orderKey, userId) == 1) then
-- 存在,说明是重复下单,返回2
return 2
end
-- 5扣库存
redis.call('incrby', stockKey, -1)
-- 6.抢购成功(保存抢购成功的用户)
redis.call('sadd', orderKey, userId)
return 0
4.压测结果
小结
提示:这里可以添加总结
本次压测,redis和rabbitMQ都是部署在本地的单机版本,在实际中,可以采用集群模式,来提高这两个中间件的并发能力。