电商网站中通常会有这样的需求,订单创建后,会给用户两小时用于支付,如果超时未支付,则要自动取消订单。最容易想到的实现思路就是用定时任务的方式,每分钟(或者更短的时间)在数据库中查询一次未支付的订单,检查距离订单创建是否超过两小时,如果超过,则把订单取消。这种方式在数据库繁忙时会增加数据库的压力,我们可以使用mq更优雅的实现这个需求。
一、 rocketmq的实现
利用rocketmq的延时消息可以很方便的实现这类需求,下面在电商项目中的模拟实现。(这里为了方便测试,把消息的延迟时间设置为1分钟)。
1.写一个方法,发送一条包含订单号的消息,设置消息的延迟时间是1分钟,即消费者1分钟后才会收到这条消息。
private void sendDelayMsg(String topic, Long orderId) throws Exception {
MQEntity entity=new MQEntity();
entity.setOrderId(orderId);
Message message=new Message(topic,tag, orderId.toString(), JSON.toJSONString(entity).getBytes());
//延迟1分钟发出
//messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
message.setDelayTimeLevel(5);
DefaultMQProducer producer = mqTemplate.getProducer();
producer.setSendMsgTimeout(20000);
producer.setVipChannelEnabled(false);
producer.send(message);
log.info("发送了一条延时消息,orderId:"+orderId);
}
2.在确认订单后,调用发送延迟消息的方法,消息的topic是在配置文件中配置的,之后消息监听器就需要监听同样topic的消息。
public Result confirmOrder(TradeOrder order) {
//1.校验订单
checkOrder(order);
//2.生成预订单
savePreOrder(order);
try {
//3.扣减库存
reduceGoodsNum(order);
//4.扣减优惠券
updateCouponStatus(order.getCouponId(),order.getOrderId());
//5.使用余额
reduceMoneyPaid(order);
//6.确认订单
updateOrderStatus(order);
//发送一个延时消息用来定时取消订单
sendDelayMsg(delayTopic,order.getOrderId());
//7.返回成功状态
return new Result(ShopCode.SHOP_SUCCESS.getSuccess(),ShopCode.SHOP_SUCCESS.getMessage());
} catch (Exception e) {
}
}
3.创建一个消息监听器,接收上一步发送的延迟消息,取到订单Id,去数据库里查询,判断订单是不是尚未支付,如果未支付,则取消订单。
@Slf4j
@Component
@RocketMQMessageListener(topic = "${mq.delay.topic}",consumerGroup = "${mq.order.consumer.group.name}",
messageModel = MessageModel.BROADCASTING)
public class DelayMsgListener implements RocketMQListener<MessageExt> {
@Autowired
private TradeOrderMapper orderMapper;
@Override
public void onMessage(MessageExt messageExt) {
log.info("接收到延迟消息成功");
String body=new String(messageExt.getBody());
MQEntity entity = JSON.parseObject(body, MQEntity.class);
Long orderId = entity.getOrderId();
TradeOrder order = orderMapper.selectById(orderId);
//检查订单状态是否是未支付
if(!ShopCode.SHOP_ORDER_PAY_STATUS_IS_PAY.equals(order.getPayStatus())){
order.setOrderStatus(ShopCode.SHOP_ORDER_CANCEL.getCode());
orderMapper.updateById(order);
log.info("取消订单成功,orderId:"+orderId);
}
}
}
4.用单元测试方法,测试订单的延时取消
@Test
public void confirmOrder() throws IOException {
TradeOrder order=new TradeOrder();
order.setUserId(345963634385633280l);
order.setCouponId(365959443973935104l);
order.setCouponPaid(new BigDecimal(50));
order.setGoodsId(345959443973935104l);
order.setGoodsNumber(1);
order.setGoodsPrice(new BigDecimal(5000));
order.setGoodsAmount(new BigDecimal(5000));
order.setShippingFee(BigDecimal.ZERO);
order.setOrderAmount(new BigDecimal(5000));
order.setMoneyPaid(new BigDecimal(100));
orderService.confirmOrder(order);
System.in.read();
}
5.执行日志如下,可以看到在14:07分发送了延迟消息,在14:08分监听器收到了延迟消息,并且做了取消订单的操作。至此,此需求就实现了。
二、rabbitmq的实现思路
rabbitmq中没有延迟消息,但可以为消息设置存活时间,当消息超过了存活时间,则被放到某个死信队列中,创建一个交换机专门用来处理这个死信队列中的消息,就可以实现同样的功能。
三、其它的类似需求
- 用户注册会员发放的优惠券于一周后失效
- 优惠活动持续三天后取消
这种在某个事件发生的一段时间后,要进行的操作,都可以用mq来实现。