点评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;
    }
相关推荐
Listen·Rain2 分钟前
用AI开发出一个AI
java·人工智能·spring boot·tomcat·intellij-idea·mybatis·visual studio
AI人工智能+电脑小能手3 分钟前
【大白话说Java面试题 第203题】【09_Zookeeper篇】第4题:ZooKeeper 的节点类型有哪些?
java·zookeeper·分布式锁·分布式协调·znode
刘小八8 分钟前
Spring AI 结构化输出:稳定生成可校验的业务 JSON
人工智能·spring·json
宸津-代码粉碎机13 分钟前
告别手动Jar部署!生产级无损热部署方案,彻底解决OOM与更新失效问题
java·大数据·开发语言·人工智能·python
米码收割机15 分钟前
【移动】线上购物移动端网站(源码+文档)【独一无二】
java·开发语言·前端·python·django
无敌秋30 分钟前
python/c++/java上云
java·c++·python
都叫我大帅哥38 分钟前
BCrypt 还是 Argon2?Spring Security 密码加密方案深度解析与实战对比
java·spring
IKUN家族9 小时前
Spring MVC(一)
java·spring·mvc
老马识途2.010 小时前
关于跨域问题的总结
java·前端
都叫我大帅哥11 小时前
Java日期时间三十年战争:从Date考古到LocalDateTime革命,以及数据库与前端的那点事儿
java