「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」
一、前言
聚焦于订单取消功能。 通过
RocketMQ延迟消息来优化原有逻辑,完善取消订单功能。
订单生成后,可以取消,取消方式分为:
- 主动取消:用户自主点击
- 被动取消:超时即取消(默认 30分钟)
被动取消方式有两种:
- 定时器:即每一定时间去扫表
- 延迟队列:延迟消息,即这个消息得一定时间之后才能收到
定时器取消订单,如图:
定时器方式存在问题:
- 订单过期时间不定,需要不停地扫表,这就给数据库增大很大压力
- 若订单数据量特别多,需要部署多台机器去扫描订单,这就特麻烦
RocketMQ 的延迟消息机制:
- 生产者发送延迟消息给消息队列
- 消费者接收延迟消息
RocketMQ 延迟队列等级对比:
| 时间 | 1s | 5s | 10s | 30s | 1m | 2m | 3m | 4m | 5m |
|---|---|---|---|---|---|---|---|---|---|
Level | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 时间 | 6m | 7m | 8m | 9m | 10m | 20m | 30m | 1h | 2h |
Level | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
RocketMQ 中设置延迟消息:
Message message = new Message();
// 设置 topic
message.setTopic(orderDelayTopic);
// 设置等级:16
message.setDelayTimeLevel(16);
二、代码实战
取消订单代码包括:
- 获取订单信息
- 检查订单状态(分布式锁)
- 更新订单状态
- 返回优惠券
- 发送取消订单通知
订单代码如下:
@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());
}
}
}