【MQ】酒店项目实战 - 订单超时

530 阅读2分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

一、前言

聚焦于订单取消功能。 通过 RocketMQ 延迟消息来优化原有逻辑,完善取消订单功能。

订单生成后,可以取消,取消方式分为:

  1. 主动取消:用户自主点击
  2. 被动取消:超时即取消(默认 30分钟)

被动取消方式有两种:

  1. 定时器:即每一定时间去扫表
  2. 延迟队列:延迟消息,即这个消息得一定时间之后才能收到

定时器取消订单,如图:

定时器方式存在问题:

  1. 订单过期时间不定,需要不停地扫表,这就给数据库增大很大压力
  2. 若订单数据量特别多,需要部署多台机器去扫描订单,这就特麻烦

rocketmq-定时器取消订单.png

RocketMQ 的延迟消息机制:

  • 生产者发送延迟消息给消息队列
  • 消费者接收延迟消息

rocketmq-延迟消息.png

RocketMQ 延迟队列等级对比:

时间1s5s10s30s1m2m3m4m5m
Level123456789
时间6m7m8m9m10m20m30m1h2h
Level101112131415161718

RocketMQ 中设置延迟消息:

Message message = new Message();
// 设置 topic
message.setTopic(orderDelayTopic);
// 设置等级:16
message.setDelayTimeLevel(16);



二、代码实战

取消订单代码包括:

  1. 获取订单信息
  2. 检查订单状态(分布式锁)
  3. 更新订单状态
  4. 返回优惠券
  5. 发送取消订单通知

订单代码如下:

@Service
public class OrderServiceImpl implements OrderService {
    private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);

    /**
     * TODO 正常获取酒店房间数据 应该调用酒店服务的rpc接口 由于没分模块则本地调用
     */
    @Autowired
    private HotelRoomService hotelRoomService;
    /**
     * 订单事件通知管理组件
     */
    @Autowired
    private OrderEventInformManager orderEventInformManager;
    /**
     * mysql dubbo服务
     */
    @Reference(version = "1.0.0",
            interfaceClass = MysqlApi.class,
            cluster = "failfast")
    private MysqlApi mysqlApi;
    /**
     * redis dubbo服务
     */
    @Reference(version = "1.0.0",
            interfaceClass = RedisApi.class,
            cluster = "failfast")
    private RedisApi redisApi;
    /**
     * TODO 本质上是走rpc远程调用 这里由于没拆分模块即本地调用
     */
    @Autowired
    private CouponService couponService;
    /**
     * 完成定时事务消息topic
     */
    @Value("${rocketmq.order.finished.topic}")
    private String orderFinishedTopic;
    /**
     * 完成订单 下发权益消息事务消息
     */
    @Autowired
    @Qualifier(value = "orderFinishedTransactionMqProducer")
    private TransactionMQProducer orderFinishedTransactionMqProducer;

    @Override
    public CommonResponse cancelOrder(String orderNo, String phoneNumber) {
        // 校验订单的状态是否可以取消
        // TODO 可以通过状态模式来优化订单的流转和保存订单操作日志
        OrderInfoDTO orderInfo = this.getOrderInfo(orderNo, phoneNumber);

        // TODO 检查时并发问题可以通过分布式锁来解决
        if (!Objects.equals(orderInfo.getStatus(), 
                            OrderStatusEnum.WAITING_FOR_PAY.getStatus())) {
            throw new BusinessException("订单不是未支付状态不能取消,订单号:" + orderNo);
        }

        // 更新订单状态
        long cancelTime = new Date().getTime() / 1000;
        this.updateOrderStatusAndCancelTime(orderNo, cancelTime, phoneNumber);

        // 判断订单是否使用优惠券 进行优惠券退回
        if (!Objects.isNull(orderInfo.getCouponId())) {
            // 退回优惠券
            couponService.backUsedCoupon(orderInfo.getCouponId(), phoneNumber);
        }

        // 发送通知消息
        orderInfo.setCancelTime((int) cancelTime);
        orderEventInformManager.informCancelOrderEvent(orderInfo);

        return CommonResponse.success();
    }
}

订单消息:

@Service
public class OrderEventInformManagerImpl implements OrderEventInformManager {

    private static final Logger LOGGER = 
        LoggerFactory.getLogger(OrderEventInformManagerImpl.class);

    @Autowired
    @Qualifier(value = "orderMqProducer")
    private DefaultMQProducer orderMqProducer;

    /**
     * 订单消息topic
     */
    @Value("${rocketmq.order.topic}")
    private String orderTopic;

    /**
     * 订单延时消息topic
     */
    @Value("${rocketmq.order.delay.topic}")
    private String orderDelayTopic;

    /**
     * 订单延时消息等级
     */
    @Value("${rocketmq.order.delay.level}")
    private Integer orderDelayLevel;

    @Override
    public void informCreateOrderEvent(OrderInfoDTO orderInfoDTO) {
        // 订单状态顺序消息
        this.sendOrderMessage(MessageTypeEnum.WX_CREATE_ORDER, orderInfoDTO);


        // 订单超时延时消息
        this.sendOrderDelayMessage(orderInfoDTO);

    }
    
    /**
     * 订单延时消息
     *
     * @param orderInfoDTO 订单信息
     */
    private void sendOrderDelayMessage(OrderInfoDTO orderInfoDTO) {
        // 发送订单付款的延时消息
        Message message = new Message();
        message.setTopic(orderDelayTopic);
        // 30分钟
        // private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
        // 延时等级从1开始 TODO 根据测试情况修改数据 5分钟
        message.setDelayTimeLevel(orderDelayLevel);
        // 内容订单号
        message.setBody(JSON.toJSONString(orderInfoDTO).getBytes(StandardCharsets.UTF_8));
        try {
            orderMqProducer.send(message);
        } catch (Exception e) {
            // 发送订单支付延时消息失败
            LOGGER.error("send order delay message fail,error message:{}", 
                         e.getMessage());
        }
    }
}