点评plus---异步消费之后可靠的生成订单

异步消费之后可靠的生成订单

来自阿星不是程序员开源项目点评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;
    }
相关推荐
vipxieliang几秒前
ValidX vs 手写验证代码:开发效率对比实测
java·spring boot·后端
vipxieliang20 分钟前
虚拟线程与 GraalVM:Spring Boot 3.x 云原生性能跃迁
java·spring boot·后端
Rain的Java大神之路21 分钟前
面试官最烦的答案:“我用 Redis + Token”!接口幂等 8 种方案 + 四层防御,把重复扣款工单干到 0
java·后端·面试
Rainmicro1 小时前
【无标题】
java·数据库·算法
驭渊的小故事1 小时前
Spring AOP 入门:从 AOP 概念到第一个耗时统计切面
java·后端·spring
空奈qwq1 小时前
【Kafka 从入门到实战】从手写一个队列到生产级消息中间件:架构、分区、消费组、零拷贝全解析
分布式·架构·kafka
天天被压力1 小时前
【A股分钟级高频数据完全手册 #09】A股历史分时交易数据接口:普通分时怎么批量下
java·人工智能·python
天天被压力2 小时前
【A股分钟级高频数据完全手册 #10】股票实时行情数据接口Python:公开源实时交易取数
java·人工智能·python
apcipot_rain2 小时前
C++转Java日记(05)
java·开发语言·c++
CoderYanger7 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis