秒杀优惠券优化点
可以看到,业务逻辑当中的每一个步骤按顺序执行,基本都是对数据库的增删改查,而数据库的反应能力本身就比较慢,极大的增加了运行压力
所以可以将业务逻辑分为两部分,判断秒杀库存是否足够和校验一人一单的资格放在redis中,redis对于用户的请求交互有较快的响应,进行完两个资格的判断,若若都通过,则创建出订单id,但这时候不算创建订单,只是相当于入场券,在进行后续的操作。大大提升了速度
这其中有几个关键的问题,如何在redis中完成资格的判断?
业务流程
首先,对于库存的判断,我们采取String结构来存储
其次,对于一人一单的资格判断,采取set结构来存储,因为set结构里面的元素不可重复,就保证了存储的用户id不能重复,其次set集合查询快,方便判断
因为同时做了很多的判断,为了保证原子性,采取lua脚本的方式来编写代码,由于在redis中只是做了简单的判断,并且保存了符合资格的优惠券id,用户id,订单id,将来在进行创建订单写操作时,只需从rendis中读取这三个id信息就能创建订单,有点像有了抢购资格,给你锁单15分钟的时间,期间只要付款就可以,极大的增加了业务的并发性。
案例:改进秒杀业务,提高并发性能
1.首先,将优惠券库存信息保存在redis中
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
// 保存优惠券
save(voucher);
// 保存秒杀信息,往数据库里写
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
// 保存优惠券id和优惠券库存到Redis中
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
2.基于lua脚本,判断库存和一人一单,决定是否锁单成功
判断一人一单,可以用set集合中的SISMEMBER,判断该用户id是否在key中存在
下图可以看出创建了一个set集合,存入了key2中三个用户,用户存在1,不存在返回0
--1.参数列表
--1.1优惠券id
local voucherId = ARGV[1]
--1.2用户id
local userId = ARGV[2]
--2.数据key
--2.1库存key
local stockKey = 'seckill:stock:' .. voucherId
--2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
--3.脚本业务
--3.1判断库存是否足够 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
--3.2 库存不足,返回错误1
return 1
end
--3.2判断用户是否之前下过单 SISMEMBER orderKey userid
if(redis.call('sismember', orderKey,userId) == 1)then
--3.3存在则说明重复下单
return 2
end
--3.4扣库存
redis.call('incrby', stockKey,-1)
--3.5下单(保存用户id)
redis.call('sadd',orderKey,userId)
3.抢单成功,将优惠券id和用户id封装到队列当中
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
//创建阻塞队列,用于存储封装的信息,后续交给下单方法处理
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 1.执行lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
int r = result.intValue();
// 2.判断结果是否为0
if (r != 0) {
// 2.1.不为0 ,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2.2.为0 ,有购买资格,把下单信息保存到阻塞队列,也就是封装起来
VoucherOrder voucherOrder = new VoucherOrder();
// 2.3.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 2.4.用户id
voucherOrder.setUserId(userId);
// 2.5.代金券id
voucherOrder.setVoucherId(voucherId);
// 2.6.放入阻塞队列
orderTasks.add(voucherOrder);
// 3.返回订单id
return Result.ok(orderId);
}
4. 创造线程池,不断地从阻塞队列中拿到消息,实现异步下单
测试,发现平均值、吞吐量都得到了很大的提升,说明从redis中进行判断可以提高并发性1.
总结
秒杀业务的优化思路:原本,应该先判断抢购是否开始,库存是否足够,是否一人一单,最后才创建订单,同时还要为怎么多的业务添加各种事务管理和锁,同时请求数据库的操作也比较慢,导致消耗了大量的时间。所以我们就将业务进行简化,异步完成,分为两部分,一部分是在redis中对抢购资格的判断,包括库存是否足够,是否符合一人一单,另一部分时下单,从组测队列中拿到符合抢购资格的用户id,优惠券id,订单id,完成下单操作。
同时,上述思路还存着一定的问题