异步消费之后可靠的生成订单
来自阿星不是程序员开源项目点评plus
之前已经对秒杀系统进行了异步化升级。
用户的购券请求会先通过 Lua 脚本完成原子性校验(库存校验、一人一单校验)以及 Redis 预扣库存。
校验成功后,系统会将订单信息封装成 Kafka 消息,由生产者发送到 Kafka Broker,
再由 Kafka 消费者异步创建订单,实现流量削峰与业务解耦。

代码:
java
/**
* 创建秒杀订单 V2版本
*
* 功能:处理秒杀消息,创建订单,扣减库存,记录订单路由信息,缓存订单数据,记录对账日志
*
* @param message Kafka消息对象,里面包装了秒杀消息体 SeckillVoucherMessage
* @return boolean 是否创建成功
*
* 注解说明:
* - @Override:重写父类方法
* - @RepeatExecuteLimit:自定义注解,防止重复执行(基于消息的uuid,避免消息重复消费)
* - @Transactional:开启事务,任何异常都会回滚
*/
@Override
/*
*@RepeatExecuteLimit 是一个防重复执行注解,
主要用于Kafka消息消费幂等控制。
*/
@RepeatExecuteLimit(name = SECKILL_VOUCHER_ORDER, keys = {"#message.uuid"})
@Transactional(rollbackFor = Exception.class)
public boolean createVoucherOrderV2(MessageExtend<SeckillVoucherMessage> message) {
// 从消息扩展对象中取出真正的业务消息体
SeckillVoucherMessage messageBody = message.getMessageBody();
// 获取用户ID(谁在抢购)
Long userId = messageBody.getUserId();
// 查询数据库:该用户是否已经成功购买过这个优惠券(且订单状态是正常状态)
VoucherOrder normalVoucherOrder = lambdaQuery()
.eq(VoucherOrder::getVoucherId, messageBody.getVoucherId()) // 优惠券ID相同
.eq(VoucherOrder::getUserId, userId) // 用户ID相同
.eq(VoucherOrder::getStatus, OrderStatus.NORMAL.getCode()) // 订单状态正常(未取消/未退款)
.one(); // 查询一条记录
// 如果查询到了,说明用户已经买过了,不允许重复购买
if (Objects.nonNull(normalVoucherOrder)) {
log.warn("已存在此订单,voucherId:{},userId:{}", normalVoucherOrder.getVoucherId(), userId);
throw new HmdpFrameException(BaseCode.VOUCHER_ORDER_EXIST); // 抛出订单已存在异常
}
// 使用乐观锁扣减库存:
// - setSql("stock = stock - 1"):库存减1
// - eq("voucher_id", ...):定位到指定的优惠券
// - gt("stock", 0):保证库存大于0才扣减(防止超卖)
// update() 返回 true 表示更新成功(扣减成功),false 表示失败(库存不足或优惠券不存在)
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", messageBody.getVoucherId())
.gt("stock", 0)
.update();
// 扣减失败,说明库存不足,抛异常触发事务回滚
if (!success) {
throw new HmdpFrameException("优惠券库存不足!优惠券id:" + messageBody.getVoucherId());
}
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(messageBody.getOrderId()); // 订单ID(雪花算法生成)
voucherOrder.setUserId(messageBody.getUserId()); // 用户ID
voucherOrder.setVoucherId(messageBody.getVoucherId()); // 优惠券ID
voucherOrder.setCreateTime(LocalDateTimeUtil.now()); // 创建时间
// 保存订单到 voucher_order 表
save(voucherOrder);
// 订单路由表作用:可能是用于后续的订单查询、分库分表、或者消息追踪
VoucherOrderRouter voucherOrderRouter = new VoucherOrderRouter();
voucherOrderRouter.setId(snowflakeIdGenerator.nextId()); // 路由表自己的ID
voucherOrderRouter.setOrderId(voucherOrder.getId()); // 关联订单ID
voucherOrderRouter.setUserId(userId); // 用户ID
voucherOrderRouter.setVoucherId(voucherOrder.getVoucherId()); // 优惠券ID
voucherOrderRouter.setCreateTime(LocalDateTimeUtil.now()); // 创建时间
voucherOrderRouter.setUpdateTime(LocalDateTimeUtil.now()); // 更新时间
// 保存路由记录
voucherOrderRouterService.save(voucherOrderRouter);
// 将订单信息存入Redis,过期时间60秒
// 用途:可能用于快速查询用户刚刚创建的订单,或者用于后续流程的快速访问
redisCache.set(
RedisKeyBuild.createRedisKey(
RedisKeyManage.DB_SECKILL_ORDER_KEY, // 缓存key前缀
messageBody.getOrderId() // 订单ID作为key后缀
),
voucherOrder, // 缓存的值:订单对象
60, // 过期时间60秒
TimeUnit.SECONDS // 时间单位:秒
);
// 记录库存扣减日志,用于后续对账(保证数据一致性)
// LogType.DEDUCT:扣减类型
// BusinessType.SUCCESS:成功状态
voucherReconcileLogService.saveReconcileLog(
LogType.DEDUCT.getCode(), // 日志类型:扣减
BusinessType.SUCCESS.getCode(), // 业务结果:成功
"order created", // 描述信息
message // 原始消息(方便追踪)
);
return true;
}