✅P299_商城业务-订单服务-定时关单完成

gong_yz大约 4 分钟谷粒商城

取消订单流程

参照下图进行说明

  • 订单创建成功,主动发消息给order-event-exchange交换机,通过路由键order.create.order发送给延时队列order.delay.queue。
  • 经过了30min后,死信会通过路由键order.release.order找到交换机order-event-exchange;
  • 然后交换机再通过路由键order.release.order将死信路由给队列order.release.order.queue,
  • 通过对队列order.release.order.queue进行监听,完成未支付订单的释放。

业务实现

初版关单实现

订单创建成功,给MQ发送关单消息

监听事件,进行关单

package com.gyz.cfmall.order.listener;

import com.gyz.cfmall.order.entity.OrderEntity;
import com.gyz.cfmall.order.service.OrderService;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author: gongyuzhuo
 * @since: 2024-02-07 19:11
 * @description:
 */
@Slf4j
@Component
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        log.info("收到过期的订单信息,准备关闭订单:{}", orderEntity.getOrderSn());
        try {
            orderService.closeOrder(orderEntity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}
@Override
public void closeOrder(OrderEntity orderEntity) {
    OrderEntity orderInfo = this.getById(orderEntity.getId());
    if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
        //代付款状态进行关单
        OrderEntity orderUpdate = new OrderEntity();
        orderUpdate.setId(orderInfo.getId());
        orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
        this.updateById(orderUpdate);
    }
}

看似目前实现的关单业务流程很正确,但是存在很大问题。下面详细阐述下订单释放&库存解锁的联动问题及解决方法。

订单释放&库存解锁**

参照下图:

订单创建后,

  1. 向MQ发送消息,1分钟以后,会进行订单解锁;
  2. 在订单创建的同时,也进行了库存锁定,这里设置的等待时间为2分钟;也就是订单解锁后的1min,库存解锁;

存在问题:

  1. 订单创建后未支付,向MQ发送订单解锁消息,但由于机器卡顿、消息延迟等原因,导致消息一致未被发送过去;
  2. 而解锁库存消息正常发送和监听,当发现订单还处于创建成功状态,就不会进行库存解锁操作,相当于不解锁消息被消费完了,
  3. 这时,即使订单被取消了,但是库存永远不会得到释放;

解决方案:

  • 释放订单服务主动将释放的订单消息推送给解锁库存服务
  • 解锁库存服务中会保存两种消息:库存服务自己的工作单详情 + 释放订单服务推送过来的释放订单

参照下图中的标号说明:

①释放订单服务通过②路由键order.release.order找到③交换机order-event-exchange,③通过④路由键order.release.other.#找到⑤队列stock.release.stock.queue,⑥解锁库存服务对释放订单进行解锁操作。

继续实现关单业务

绑定交换机与队列

cfmall-order/src/main/java/com/gyz/cfmall/order/config/MyMQConfig.java

    /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOtherBinding() {

        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }

关单发送解锁库存消息

cfmall-order/src/main/java/com/gyz/cfmall/order/service/impl/OrderServiceImpl.java

监听释放订单

cfmall-ware/src/main/java/com/gyz/cfmall/ware/listener/StockReleaseListener.java

    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {

        log.info("******收到订单关闭,准备解锁库存的信息******");

        try {
            wareSkuService.releaseOrder(orderTo);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

解锁库存

cfmall-ware/src/main/java/com/gyz/cfmall/ware/service/impl/WareSkuServiceImpl.java

    /**
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
     * 导致卡顿的订单,永远都不能解锁库存
     * @param orderTo
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void releaseOrder(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        //查一下最新的库存解锁状态,防止重复解锁库存
        WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOne(
                new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn)
        );

        //按照工作单的id找到所有 没有解锁的库存,进行解锁
        Long id = orderTaskEntity.getId();
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id).eq("lock_status", 1));

        for (WareOrderTaskDetailEntity taskDetailEntity : list) {
            unLockStock(taskDetailEntity.getSkuId(),
                    taskDetailEntity.getWareId(),
                    taskDetailEntity.getSkuNum(),
                    taskDetailEntity.getId());
        }
    }

至此完成定时关单业务。