一、前言:没有 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) |
| 降级策略 | 队列满时,直接返回"系统繁忙" |
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!