点评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;
    }
相关推荐
一个做软件开发的牛马11 分钟前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户37215742613511 分钟前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户37215742613520 分钟前
Java 打印 Word 文档:从基础打印到高级设置
java
用户35218024547516 小时前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程
东坡白菜19 小时前
破局全栈:一个前端开发的Java入门实战记录(1)
java·全栈
唐青枫19 小时前
Java Tomcat 实战指南:从 Servlet 容器到 Spring Boot 部署
java
wsaaaqqq20 小时前
roudan:自由选择实体、灵活操作数据、快速写入数据库的 Java 框架
java
plainGeekDev1 天前
null 判断 → Kotlin 可空类型
android·java·kotlin
糖拌西瓜皮1 天前
Java开发者视角:深入理解Node.js异步编程模型
java·后端·node.js
plainGeekDev1 天前
getter/setter → Kotlin 属性
android·java·kotlin