秒杀优化-基于阻塞队列实现秒杀优化

一、前言:没有 Kafka/RabbitMQ,也能做异步秒杀?

很多团队想优化秒杀系统,但受限于:

  • 公司未部署消息队列(MQ)
  • 运维复杂度高
  • 项目规模小,不想引入额外中间件

好消息是:用 Java 自带的 阻塞队列(BlockingQueue),就能实现轻量级异步秒杀!

本文将手把手教你基于 ArrayBlockingQueue 构建高性能秒杀系统,无需任何外部依赖,轻松应对千级并发。


二、核心思想:内存队列 + 异步消费者

核心原则
HTTP 请求只做"快速校验 + 入队",后续操作由后台线程异步消费!

架构图:

复制代码
用户请求
    ↓
[Controller]
    ├── 1. 校验资格(登录、一人一单)
    ├── 2. Redis 扣减预库存(Lua 原子)
    └── 3. 将请求封装成任务,放入 BlockingQueue
        ↓
[后台消费者线程](独立线程池)
    ├── 1. 从队列取任务
    ├── 2. 创建订单/优惠券记录
    ├── 3. 扣减 DB 库存
    └── 4. 发送通知

🔑 关键点HTTP 线程不等待业务完成,立即返回!


三、为什么选择阻塞队列?

方案 优点 缺点 适用场景
RabbitMQ / Kafka 高可靠、持久化、可扩展 需运维、有学习成本 大型系统
BlockingQueue 零依赖、简单高效、内存快 进程内、重启丢失、容量有限 中小系统、快速上线

💡 对于优惠券秒杀、抽奖、报名等非强一致场景,阻塞队列完全够用!


四、完整代码实现(Spring Boot)

步骤 1:定义秒杀任务模型

java 复制代码
public class SeckillTask {
    private Long userId;
    private Long couponId;
    private String requestId; // 幂等 ID
    private long createTime = System.currentTimeMillis();

    // 构造方法、getter/setter 略
}

步骤 2:创建阻塞队列与消费者线程池

java 复制代码
@Configuration
public class SeckillQueueConfig {

    // 创建有界阻塞队列(防止 OOM)
    @Bean
    public BlockingQueue<SeckillTask> seckillTaskQueue() {
        return new ArrayBlockingQueue<>(10000); // 最多缓存 1 万任务
    }

    // 启动消费者线程
    @PostConstruct
    public void startConsumer() {
        Thread consumerThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从队列阻塞获取任务
                    SeckillTask task = seckillTaskQueue().take();
                    handleSeckillTask(task);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                } catch (Exception e) {
                    log.error("处理秒杀任务异常", e);
                }
            }
        });
        consumerThread.setDaemon(true); // 守护线程
        consumerThread.setName("seckill-consumer");
        consumerThread.start();
    }

    @Autowired
    private CouponRecordService recordService;

    private void handleSeckillTask(SeckillTask task) {
        try {
            // 1. 幂等校验
            if (recordService.existsByRequestId(task.getRequestId())) {
                return;
            }

            // 2. 兜底校验(防止 Redis 状态漂移)
            if (recordService.hasReceived(task.getUserId(), task.getCouponId())) {
                return;
            }

            // 3. 落库:创建记录 & 扣减 DB 库存
            recordService.createCouponRecord(
                task.getRequestId(),
                task.getUserId(),
                task.getCouponId()
            );

            // 4. 可选:发送通知
            // notificationService.send(task.getUserId());

        } catch (Exception e) {
            log.error("秒杀任务处理失败, task={}", task, e);
            // 可扩展:失败重试、写入日志文件等
        }
    }
}

步骤 3:秒杀入口(快进快出)

java 复制代码
@RestController
public class SeckillController {

    @Autowired
    private BlockingQueue<SeckillTask> seckillTaskQueue;

    @Autowired
    private SeckillService seckillService;

    @PostMapping("/seckill/{couponId}")
    public Result<String> seckill(@PathVariable Long couponId,
                                  @RequestHeader("userId") Long userId) {

        // 1. 快速资格校验
        if (seckillService.hasReceived(userId, couponId)) {
            return Result.fail("您已领取过该优惠券");
        }

        // 2. Redis Lua 原子扣减预库存
        if (!seckillService.tryDecreaseStock(couponId)) {
            return Result.fail("手慢了,已抢光!");
        }

        // 3. 封装任务并入队
        SeckillTask task = new SeckillTask(
            userId, 
            couponId, 
            UUID.randomUUID().toString()
        );

        boolean offered = seckillTaskQueue.offer(task);
        if (!offered) {
            // 队列满,可降级:直接拒绝 or 落库兜底
            return Result.fail("系统繁忙,请稍后再试");
        }

        // 4. 立即返回!
        return Result.success("提交成功,请稍后查看领取记录");
    }
}

整个 HTTP 请求耗时 < 30ms!


五、关键技术点解析

1️⃣ 有界队列防 OOM

  • 使用 ArrayBlockingQueue(有界),避免无限制堆积导致内存溢出
  • 队列满时,offer() 返回 false,可立即拒绝新请求(保护系统)

2️⃣ 幂等性保障

  • 每个任务携带 requestId(全局唯一)
  • 消费前检查 DB 是否已处理,避免重复发券

3️⃣ 兜底校验

  • 即使 Redis 扣减成功,消费时仍需二次校验
  • 防止因进程重启、Redis 故障导致状态不一致

4️⃣ 守护线程 + 异常捕获

  • 消费者线程设为 daemon,随 JVM 退出
  • 捕获所有异常,避免线程意外终止

六、优缺点分析

✅ 优点:

  • 零外部依赖:仅用 JDK 并发包
  • 开发简单:几十行代码搞定异步
  • 性能高:内存操作,微秒级入队
  • 资源可控:队列大小、线程数可配置

❌ 缺点:

  • 进程内队列:应用重启,未消费任务丢失
  • 无持久化:不适合金融级强一致场景
  • 单机瓶颈:无法跨实例共享队列(集群需每台独立处理)

📌 建议

  • 适用于优惠券、抽奖、报名等容忍少量丢失的场景
  • 若需高可靠,后期可平滑迁移到 RabbitMQ/Kafka

七、生产环境增强建议

问题 解决方案
任务丢失 定期将队列任务快照到本地文件(如每 10s)
消费积压 增加消费者线程(使用线程池)
监控缺失 暴露队列 size 指标(/actuator/metrics
降级策略 队列满时,直接返回"系统繁忙"

八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
清水白石0086 小时前
深入解析 LRU 缓存:从 `@lru_cache` 到手动实现的完整指南
java·python·spring·缓存
静听山水7 小时前
Redis核心数据结构-Set
数据结构·数据库·redis
无尽的沉默7 小时前
Redis下载安装
数据库·redis·缓存
曾经的三心草7 小时前
redis-9-集群
java·redis·mybatis
czlczl200209257 小时前
增删改查时如何提高Mysql与Redis的一致性
数据库·redis·mysql
静听山水7 小时前
Redis核心数据结构
数据结构·数据库·redis
yuanmenghao7 小时前
Linux 性能实战 | 第 10 篇 CPU 缓存与内存访问延迟
linux·服务器·缓存·性能优化·自动驾驶·unix
静听山水8 小时前
Redis核心数据结构-Hash
数据结构·redis·哈希算法