这次改造的核心目标是:把秒杀的数据库操作异步化,用阻塞队列 + 线程池实现「请求快速响应、后台异步处理订单」,彻底解决秒杀场景下的数据库压力瓶颈

一、新增核心组件:阻塞队列与线程池

// 阻塞队列:存放秒杀订单任务
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
// 异步处理线程池
private static ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

关键说明:

  1. ArrayBlockingQueue

    1. 有界队列,固定容量 1024*1024,可承载百万级秒杀订单任务,防止无限堆积导致内存溢出;

    2. 具备阻塞特性:队列满时生产者入队会阻塞,队列空时消费者取任务会阻塞;

    3. 天然适配生产者 - 消费者模型:秒杀接口作为生产者往队列放订单,后台线程作为消费者从队列取订单处理;

    4. 线程安全,无需手动加锁即可在多线程环境下安全入队、出队。

  2. newSingleThreadExecutor

    1. 内部只有一个工作线程,串行依次处理每一个秒杀订单;

    2. 保证订单按到达顺序依次处理,避免多线程并发操作同一张订单表、同一条商品库存记录,引发数据错乱、超卖、重复下单等问题;

    3. 线程池全局静态唯一,项目生命周期内只创建一次,复用线程,减少线程创建销毁开销。

二、类初始化时启动消费者线程

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

详解

  • @PostConstruct:Spring 生命周期注解,当前 Bean 被 Spring 容器初始化完成后立刻自动执行,不用手动调用;

  • 项目一启动就自动开启消费者监听线程,提前待命,不用等用户发起秒杀才创建线程;

  • 将自定义任务 VoucherOrderHandler 提交给单线程池,开始无限循环监听阻塞队列,等待秒杀订单任务进入。

三、消费者线程实现:VoucherOrderHandler

private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                // 1. 从阻塞队列获取订单信息(队列空时自动阻塞)
                VoucherOrder voucherOrder = orderTasks.take();
                // 2. 处理订单
                handleVoucherOrder(voucherOrder);
            } catch (Exception e) {
                log.error("处理订单异常", e);
            }
        }
    }
}

解析

  • while(true):开启死循环常驻监听,只要项目不停止,就一直监听队列任务;

  • orderTasks.take()阻塞式获取元素,队列里没有订单任务时,线程会进入阻塞休眠状态,不占用 CPU 资源;一旦有订单入队立刻被唤醒处理;

  • try-catch 捕获异常:单个订单处理报错不影响整体循环,避免一个订单异常导致整个秒杀消费线程挂掉;

  • 所有数据库下单、扣库存逻辑都放到异步线程中执行,和前端请求主线程完全解耦。

四、订单处理方法:handleVoucherOrder

private void handleVoucherOrder(VoucherOrder voucherOrder) {
    // 1. 获取用户ID(从订单对象中取,避免ThreadLocal失效)
    Long userId = voucherOrder.getUserId();
    // 2. 创建Redisson锁
    RLock lock = redissonClient.getLock("lock:order:" + userId);
    // 3. 尝试获取锁
    boolean isLock = lock.tryLock();
    if (!isLock) {
        log.error("不允许重复下单");
        return;
    }
    try {
        // 4. 获取代理对象(解决事务失效问题)
        proxy.createVoucherOrder(voucherOrder);
    } finally {
        // 5. 释放锁
        lock.unlock();
    }
}

关键变更:

1、用户 ID 不从 UserHolder 获取

异步线程不属于前端请求的主线程,ThreadLocal 中的用户信息会失效、拿不到数据,所以只能从已经封装好的 voucherOrder 订单对象中获取用户 ID。

2、引入 Redisson 分布式锁

即使 Redis Lua 脚本已经做了一人一单前置校验,这里仍加分布式锁做业务兜底,防止网络重试、重复请求导致重复下单;保证同一个用户同一时间只能处理一个秒杀订单。

3、必须使用代理对象调用事务方法

异步线程中如果直接 this.createVoucherOrder(),会绕过 Spring AOP 代理,导致 @Transactional 事务注解失效;通过 AopContext.currentProxy() 获取当前类代理对象调用方法,才能让事务生效,保证扣库存、创订单的原子性。

4、finally 释放锁

保证无论业务正常执行还是抛出异常,分布式锁都一定会释放,避免死锁。

五、秒杀入口方法改造:快速响应 + 入队

@Override
public Result seckillVoucher(Long voucherId) {
    // 获取用户ID
    Long userId = UserHolder.getUser().getId();
    // 1. 执行Lua脚本(库存、一人一单校验+扣减)
    Long result = stringRedisTemplate.execute(...);
    int r = result.intValue();
    if (r != 0) {
        return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
    }

    // 2. 秒杀成功,构建订单对象
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);

    // 3. 放入阻塞队列(生产者)
    orderTasks.add(voucherOrder);

    // 4. 返回订单ID,直接响应前端
    return Result.ok(orderId);
}

关键变更:

  1. 所有复杂校验下沉到 Redis + Lua

原子完成:库存判断、一人一单判断、Redis 库存扣减,杜绝超卖和重复下单,性能远高于 Java 层多次判断 DB。

  1. 请求主线程不再操作数据库

只做 Lua 脚本调用、封装订单、入队操作,全程都是内存和 Redis 操作,接口响应速度极快

  1. 异步解耦

      订单真正入库、扣数据库库存全部交给后台消费线程慢慢处理,前端不用等待数据库 IO,体验无感。

  1. 全局唯一订单 ID

通过 Redis 全局 ID 生成器生成 orderId,提前返回给前端,前端可凭订单 ID 查询后续订单状态。

六、创建订单方法改造:异步处理 + 参数变更

@Override
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
    // 1. 从订单对象获取用户ID和优惠券ID
    Long userId = voucherOrder.getUserId();
    Long voucherId = voucherOrder.getVoucherId();

    // 2. 一人一单校验(兜底)
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    if (count > 0) {
        log.error("用户已经购买过一次!");
        return;
    }

    // 3. 扣减库存(乐观锁)
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId).gt("stock", 0)
            .update();
    if (!success) {
        log.error("库存不足");
        return;
    }

    // 4. 保存订单
    save(voucherOrder);
}

改造说明

  • 方法参数由原来的优惠券 ID,改为完整订单对象,适配异步线程无 ThreadLocal 的场景;

  • 保留数据库层一人一单兜底校验,形成 Redis 前置拦截 + DB 最终兜底的双层防护;

  • 采用乐观锁扣库存gt("stock",0) 保证库存大于 0 才扣减,防止超卖;

  • 添加 @Transactional 事务注解,保证扣库存、新增订单要么同时成功,要么同时回滚,数据一致不错乱。

总结

这次改造的核心目的:通过阻塞队列 + 线程池实现秒杀请求的异步处理,把数据库操作从请求主线程剥离,实现「前端快速响应、后台异步处理订单」,大幅提升系统并发能力,同时解决秒杀场景下的数据库压力瓶颈。

💡 补充:这就是典型的生产者 - 消费者模式,Lua 脚本是「快速校验 + 生产者」,阻塞队列和线程池是「消费者」,实现了高并发场景下的流量削峰。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐