基于上一篇博客,用阻塞队列实现异步下单

发布于:2024-07-02 ⋅ 阅读:(11) ⋅ 点赞:(0)

在上一篇博客中,我们介绍了如何利用 Redis 和 Lua 脚本来高效处理秒杀活动中的高并发请求,保证用户体验。本文将进一步优化秒杀系统,通过引入阻塞队列实现异步下单,从而提高系统的整体性能和稳定性。

引言

秒杀活动往往伴随着极高的并发请求,对系统的性能和稳定性提出了巨大挑战。同步处理订单请求可能导致数据库压力过大,影响系统响应时间。为了缓解这一问题,我们可以采用异步下单的方式,将订单请求先放入阻塞队列,由后台线程逐一处理,从而降低数据库的瞬时压力。

方案设计

基本思路

  1. 用户发起秒杀请求,先通过 Redis Lua 脚本进行资格判断。
  2. 通过 Lua 脚本判断用户是否有购买资格,并扣减库存。
  3. 将订单信息放入阻塞队列中,由后台线程异步处理订单创建和数据库操作。
  4. 返回订单 ID 给用户。

具体实现

Lua 脚本

Lua 脚本的逻辑保持不变,继续用于判断秒杀资格和扣减库存。

Java 代码

在 Java 代码中,我们通过阻塞队列实现异步下单,并利用 Redisson 分布式锁来确保订单操作的线程安全。

@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    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);
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    VoucherOrder voucherOrder = orderTasks.take();
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("创建订单失败", e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("不允许重复下单");
            return;
        }
        try {
            proxy.crateVoucherOrder(voucherOrder);
        } finally {
            lock.unlock();
        }
    }

    private IVoucherOrderService proxy;

    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        if (r != 0) {
            return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        orderTasks.add(voucherOrder);
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId);
    }

    @Override
    @Transactional
    public void crateVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0) {
            log.error("不允许重复下单!");
            return;
        }
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",
                        voucherOrder.getVoucherId())
                .gt("stock", 0).update();
        if (!success) {
            log.error("库存不足!");
            return;
        }
        save(voucherOrder);
    }
}

代码详解

  1. 初始化阻塞队列和线程池

    • 使用 BlockingQueueExecutorService 实现一个单线程的订单处理机制,在服务初始化时启动订单处理线程。
  2. 秒杀请求处理

    • 用户发起秒杀请求时,首先通过 Lua 脚本判断秒杀资格和扣减库存。
    • 如果有购买资格,将订单信息放入阻塞队列中。
  3. 订单处理线程

    • 订单处理线程从阻塞队列中取出订单,并在获取到用户锁后创建订单,防止同一用户重复下单。
  4. 事务处理

    • 在订单处理方法中使用事务管理,确保订单创建和库存扣减的原子性。

结论

通过引入阻塞队列实现异步下单,我们有效地减少了数据库的瞬时压力,提高了系统的整体性能和稳定性。这种方法不仅适用于秒杀活动,还可以推广到其他高并发场景,如抢购、促销活动等。希望本文对您理解和实现高并发系统有所帮助。

可能出现的问题

我在一次批量用一千个线程去抢优惠卷的时候发现,优惠卷没有抢完,初步判断是阻塞队列的大小过小,内存的限制问题。