黑马点评_秒杀优化_基于阻塞队列实现秒杀异步下单

系列博客目录



22.秒杀优化-异步秒杀思路

目前的代码如下:

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }

        return createVoucherOrder(voucherId);
    }

@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 5.一人一单
    Long userId = UserHolder.getUser().getId();

    // 创建锁对象
    RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    // 尝试获取锁
    boolean isLock = redisLock.tryLock();
    // 判断
    if(!isLock){
        // 获取锁失败,直接返回失败或者重试
        return Result.fail("不允许重复下单!");
    }

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    } finally {
        // 释放锁
        redisLock.unlock();
    }
}

如下图所示,平均耗时400多ms,所以速度很慢。
在这里插入图片描述
目前的思路如下图所示:现在是串行进行下面图中的每一步,里面有四步都需要走数据库,还有写操作,所以很慢。比如现在饭店里有个小姐姐,她既要负责接待顾客,点单,下单,做饭,上菜,她一个人负责完整的业务。现在的效率很低,所以我们可以再请一个人,小姐姐负责照顾顾客,点单下单,后厨拿到单之后负责做菜,做好菜,再上菜即可。我们应该把下面流程分为两部分,一个是秒杀资格判断,一个是减库存+创建订单。JAVA一个线程没法同时处理两部分事情,所以要在原来线程的基础上开辟一个新的线程。
在这里插入图片描述
优化后的流程图如下:由于秒杀资格判断需要数据库中的数据,如果还是每次从MySQL中拿数据,太慢了,所以可以使用Redis来存储一部分数据,用于秒杀资格的判断。这时候为了分离两部分,我们需要有个“小票”使得两部分连接起来,可以使用阻塞队列,用来记录谁买了什么,还要返回订单id,让顾客可以直接买单了,这时候订单可能还没有创建或者写入数据库,但未来我们保证可以完成创建。
在这里插入图片描述

如何使用Redis实现判断秒杀库存和校验一人一单。
判断秒杀库存,只需把秒杀券的库存通过String类型放入Redis即可。
校验一人一单,要针对每个秒杀券保存用户id,还要保持value的唯一性。所以使用Set类型的Redis。

在这里插入图片描述

上图中,为了保证原子性,使用了Lua脚本。

23.秒杀优化-基于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);
    // 新增:保存秒杀库存到Redis中
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

在这里插入图片描述
编写Lua脚本,为之后调用做准备。

-- 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.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
}

@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");
    //TODO 保存阻塞队列 之后再编写这部分代码
    
    // 3.返回订单id
    return Result.ok(orderId);
}

24.秒杀优化-基于阻塞队列实现秒杀异步下单

.......
// 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);
为了在这个类一初始化,就开始执行VoucherOrderHandler代码(也就是开始执行VoucherOrderHandler里的run方法,线程一被创建就执行run方法)。使用@PostConstruct注解
@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
private class VoucherOrderHandler implements Runnable{

@Override
public void run() {
   while (true){
      try {
            // 1.获取队列中的订单信息
            VoucherOrder voucherOrder = orderTasks.take();//有元素才会往下执行,要不然会卡在这里。
            // 2.创建订单
            createVoucherOrder(voucherOrder);
       } catch (Exception e) {
            log.error("处理订单异常", e);
       }
    }
}

private void createVoucherOrder(VoucherOrder voucherOrder) {
    Long userId = voucherOrder.getUserId();
    Long voucherId = voucherOrder.getVoucherId();
    // 创建锁对象
    RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    // 尝试获取锁
    boolean isLock = redisLock.tryLock();
    // 判断
    if (!isLock) {
        // 获取锁失败,直接返回失败或者重试
        log.error("不允许重复下单!");
        return;
    }

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            log.error("不允许重复下单!");
            return;
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足!");
            return;
        }

        // 7.保存订单 不像之前需要创建订单了,订单已经创建完毕了
        save(voucherOrder);
    } finally {
        // 释放锁
        redisLock.unlock();
    }
}

总结

  1. 秒杀业务的优化思路是什么?
    • 先利用Redis完成库存余量、一人一单判断,完成抢单业务
    • 再将下单业务放入阻塞队列,利用独立线程异步下单
  2. 基于阻塞队列的异步秒杀存在哪些问题?
    • 内存限制问题(我们现在基于内存保存数据,如果不设置阻塞队列的大小可能会导致内存溢出,但设置了大小,又可能会不够用)
    • 数据安全问题(我们现在基于内存保存数据,如果内存宕机,数据就消失了,可能返回了订单ID,但是订单的信息不会在未来保存在数据库中)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值