点评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;
    }
相关推荐
zzh___zzh6 小时前
Spring Boot资源路径与ClassPathResource详解
java
学渣超6 小时前
微服务日志智能诊断系统(八) 全链路追踪——request_id驱动的跨服务诊断
java·后端·agent
ACGkaka_6 小时前
Spring Boot 实战(四十一):集成 BPMN(Flowable+bpmn-js)
java·spring boot·后端
学渣超6 小时前
微服务日志智能诊断系统(九) 让 LLM 成为日志分析专家
java·后端·agent
学渣超6 小时前
微服务日志智能诊断系统(七) 日志 RAG 引擎——蒸馏、索引与精准检索
java·后端·agent
掉鱼的猫6 小时前
代码审查 Agent Harness 实战:AI 自动 Code Review
java·llm·agent
学渣超6 小时前
微服务日志智能诊断系统(六) Fast Path vs Slow Path——双路径日志诊断策略
java·后端·agent
添砖java_8576 小时前
网页聊天室测试报告
java·spring boot·spring·maven
hai3152475436 小时前
九章编译法:DEEPSEEK V3.2汇编编译实例
java·开发语言
ONE_SIX_MIX6 小时前
lightweight-charts-onesixth v2.8.1 → v3.1.1 更新总览
java·前端·python