目录
在秒杀系统中,用户的并发请求数量往往远超系统的承受能力。如果直接在用户请求中同步执行订单创建操作,会导致数据库的瞬时压力过大,从而影响系统的整体性能和用户体验。为了解决这个问题,本文将介绍如何通过引入阻塞队列来实现异步下单,并进一步优化系统的并发性能。
一、引入阻塞队列的设计思路
在高并发场景下,秒杀请求通常分为两个阶段:
-
秒杀资格判断:
- 用户发起秒杀请求时,通过 Redis 和 Lua 脚本对用户进行秒杀资格的判断。
- 在该步骤中,主要操作包括库存判断、用户重复判断和库存扣减等。
-
订单创建:
- 用户通过秒杀资格判断后,需要生成订单并将其持久化到数据库中。
- 如果直接在请求线程中进行订单创建操作,数据库在短时间内可能会因为过多的请求而出现阻塞或性能下降。
为了解决这一问题,我们引入了阻塞队列来进行异步下单。具体流程如下:
- 用户请求通过 Lua 脚本进行秒杀资格判断,并扣减 Redis 中的库存。
- 将订单信息(如用户 ID、优惠券 ID)放入阻塞队列中。
- 后台线程从阻塞队列中逐一取出订单信息,并创建订单。
- 通过引入阻塞队列和异步处理,避免了数据库瞬时写入压力过大的问题,从而提升系统的整体性能。
二、基于阻塞队列的异步下单代码实现
下面是完整的 Java 实现代码,通过阻塞队列和后台线程实现异步订单创建。
@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);
}
}
三、代码详解
-
Lua 脚本判断秒杀资格:
- 使用 Redis 和 Lua 脚本完成秒杀资格判断和库存扣减,避免了数据库在高并发场景下的竞争。
-
阻塞队列存储订单信息:
- 通过
BlockingQueue<VoucherOrder> orderTasks
来存放用户秒杀成功的订单信息。阻塞队列能够很好地缓冲并发请求,防止订单数据丢失。
- 通过
-
后台线程异步处理订单:
- 通过后台线程
VoucherOrderHandler
来从阻塞队列中逐一取出订单信息,并进行订单创建。 - 使用 Redisson 分布式锁来保证用户订单操作的线程安全性。
- 通过后台线程
-
事务管理和数据一致性:
- 在订单创建方法中使用了 Spring 的
@Transactional
注解,确保订单创建和库存扣减的操作具有原子性和一致性。
- 在订单创建方法中使用了 Spring 的
四、结论与优化建议
通过引入阻塞队列和异步下单处理,我们有效地减少了数据库的瞬时写入压力,提高了系统的整体性能和稳定性。该方案不仅适用于秒杀活动,还可以推广到其他高并发场景(如抢购、促销活动等)。
可能的优化点:
-
增加阻塞队列容量:
- 在高并发测试中,如果阻塞队列的容量过小(如 1024),可能会出现队列溢出的问题。建议在内存允许的情况下,适当增大队列容量。
-
引入消息队列:
- 如果并发量特别高,可以考虑将订单信息写入 Kafka 或 RabbitMQ 等消息队列,以进一步提升系统的扩展性和性能。