思路
未优化前,业务按照步骤依次执行,所需要的时间是所有业务时间之和,这样做的话,原本那些比如判断秒杀库存,校验一人一单的操作就会等待,直到之前的业务流程走到它时,才开始执行,这样也就会使得整个流程执行时间变长,影响体验。
所以,我们优化的思路就是,将判断库存和校验一人一单的活,交给redis来做 对数据库的操作就让主线程开启一个新线程来异步帮我们进行操作。
具体实现
1.优惠券新增时,将优惠劵的数量信息,保存到redis中,这里用String型保存即可,这样之后判断是否有库存只需要查询redis中库存数是否大于0即可。
@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);
// 保存秒杀库存到Redis中
System.out.println("保存优惠卷信息到redis");
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
2.对于一人一单的业务,我们可以用set这个不重复集合类型,只要每次判断set中是否有用户信息,如果有就不能买,如果没有就有资格买。
对于上述两个操作,我们要保证他的原子性,所以,我们最好用lua脚本进行编写业务逻辑
(由于博主本人不太会lua所以就用java代码先实现了,这不能保证原子性)
public boolean hasQualification(long voucherId,long userId){
// 判断库存
String s = stringRedisTemplate.opsForValue().get(SECKILL_STOCK_KEY + voucherId);
int number = Integer.parseInt(s);
if(number<1){
return false;
}
// 判断一人一单
Boolean buyMember = stringRedisTemplate.opsForSet().isMember("buyMember", String.valueOf(userId));
if(!buyMember) {//有资格就先将用户信息保存到set中
// 将订单信息保存到set中
stringRedisTemplate.opsForSet().add("buyMember", String.valueOf(userId));
}
return !buyMember;
}
3.判断是否有资格购买时,主线程任务就结束了
有资格:将订单信息和用户放入阻塞队列中,拿到代理对象(由于之后事务要用到代理对象,所以在主线程中就先拿到),直接返回前端信息
没资格:直接返回信息
@Override
public Result seckillVoucher(Long voucherId) {
// 拿到用户id
Long userId = UserHolder.getUser().getId();
// 判断是否有资格
boolean hasQ = this.hasQualification(voucherId, userId);
if(!hasQ){//没资格直接返回
return Result.fail("用户已经下单过或优惠券已经抢完");
}
// 有资格
// 封装订单信息
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(redisIdWorker.nextId("order"));//订单id
voucherOrder.setUserId(userId);//用户id
voucherOrder.setVoucherId(voucherId);//优惠券id
// 将订单信息放入阻塞队列中
orderTasks.add(voucherOrder);
// 获取代理对象
proxy= (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(voucherOrder.getId());
}
4.开启子线程,并且让其轮询阻塞队列,注意子线程最好在类初始化时就执行,这时可以用到
@PostConstruct注解
之后子线程执行数据库操作即可!
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);//阻塞队列
// 创建线程池,异步执行数据库操作
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
// 让类在初始化之后立即就执行请求阻塞队列的业务
@PostConstruct//让类在初始化时就进行执行该注解标记的方法
private void init(){
System.out.println("执行到这!");
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());//在类初始化时就提交该任务
}
// 创建线程任务
private class VoucherOrderHandle implements Runnable{
@Override
public void run() {
while (true){
// 1.获取对列中的订单信息
try {
VoucherOrder order = orderTasks.take();//拿到阻塞队列中订单信息,take:拿到并删除阻塞队列的头部,如果没有则阻塞等待
// 创建订单
createOrder(order);
} catch (InterruptedException e) {
log.error("阻塞队列异常");
}
}
}
private void createOrder(VoucherOrder order) {
// 拿到用户id
Long userId = order.getUserId();
// 创建锁对象
RLock lock = redissonClient.getLock("order:" + userId);
boolean getLock = false;
try {
getLock = lock.tryLock(1L, 20L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!getLock){
// 不成功
log.error("获取锁异常");
return;
}
try {
proxy.createVoucherOrder(order);
}finally {
lock.unlock();
}
}
}
@Transactional
@Override
public void createVoucherOrder(VoucherOrder order) {
// 扣减库存
SeckillVoucher voucher = seckillVoucherService.getById(order.getVoucherId());
System.out.println(voucher.getStock());
boolean success = voucherOrderMapper.updateStock(voucher.getStock());
if(!success){
log.error("抢购失败,轻重新抢购!");
}
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(redisIdWorker.nextId("order"));
voucherOrder.setUserId(order.getUserId());
voucherOrder.setVoucherId(order.getVoucherId());
// 将订单信息保存
save(voucherOrder);
}