✅P299_商城业务-订单服务-定时关单完成
大约 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);
}
}
看似目前实现的关单业务流程很正确,但是存在很大问题。下面详细阐述下订单释放&库存解锁的联动问题及解决方法。
订单释放&库存解锁**
参照下图:
订单创建后,
- 向MQ发送消息,1分钟以后,会进行订单解锁;
- 在订单创建的同时,也进行了库存锁定,这里设置的等待时间为2分钟;也就是订单解锁后的1min,库存解锁;
存在问题:
- 订单创建后未支付,向MQ发送订单解锁消息,但由于机器卡顿、消息延迟等原因,导致消息一致未被发送过去;
- 而解锁库存消息正常发送和监听,当发现订单还处于创建成功状态,就不会进行库存解锁操作,相当于不解锁消息被消费完了,
- 这时,即使订单被取消了,但是库存永远不会得到释放;
解决方案:
- 释放订单服务主动将释放的订单消息推送给解锁库存服务。
- 解锁库存服务中会保存两种消息:库存服务自己的工作单详情 + 释放订单服务推送过来的释放订单
参照下图中的标号说明:
①释放订单服务通过②路由键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());
}
}
至此完成定时关单业务。