延迟消息

什么是延迟消息?

延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息。

延迟任务:设置在一定时间之后才执行的任务。

死信交换机

当一个队列中的消息满足下列情况之一时, 就会成为死信 (dead letter):

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息(达到了队列或消息本身设置的过期时间), 超时无人消费
  • 要投递的队列消息堆积满了, 最早的消息可能成为死信

如果队列通过dead-letter-exchange属性指定了一个交换机, 那么该队列中的死信就会投递到这个交换机中。这个交换机称为死信交换机 (Dead Letter Exchange, 简称DLX)。

延迟消息插件

RabbitMQ的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。

发送消息时需要通过消息头x-delay来设置过期时间:

取消超时订单

设置30分钟后检测订单支付状态实现起来非常简单,但是存在两个问题:

  • 如果并发较高,30分钟可能堆积消息过多,对MQ压力很大
  • 大多数订单在下单后1分钟内就会支付,但是却需要在MQ内等待30分钟,浪费资源

优化方案

代码实现:

以下内容是黑马商城的内容,这里我们学习一下是如何实现当前的?

由于我们要多次发送延迟消息,因此需要先定义一个记录消息延迟时间的消息体,出于通用性考虑,我们将其定义到hm-common模块下:代码如下:

package com.hmall.common.domain;
import com.hmall.common.utils.CollUtils;
import lombok.Data;
import java.util.List;
@Data
public class MultiDelayMessage<T> {
  /**
  * 消息体
  */
  private T data;
  /**
  * 记录延迟时间的集合
  */
  private List<Long> delayMillis;
    public MultiDelayMessage(T data, List<Long> delayMillis) {
      this.data = data;
      this.delayMillis = delayMillis;
      }
      public static <T> MultiDelayMessage<T> of(T data, Long ... delayMillis){
        return new MultiDelayMessage<>(data, CollUtils.newArrayList(delayMillis));
          }
          /**
          * 获取并移除下一个延迟时间
          * @return 队列中的第一个延迟时间
          */
          public Long removeNextDelay(){
          return delayMillis.remove(0);
          }
          /**
          * 是否还有下一个延迟时间
          */
          public boolean hasNextDelay(){
          return !delayMillis.isEmpty();
          }
          }
定义常量

无论是消息发送还是接收都是在交易服务完成,因此我们在trade-service中定义一个常量类,用于记录交换机、队列、RoutingKey等常量:内容如下:

package com.hmall.trade.constants;
public interface MqConstants {
String DELAY_EXCHANGE = "trade.delay.topic";
String DELAY_ORDER_QUEUE = "trade.order.delay.queue";
String DELAY_ORDER_ROUTING_KEY = "order.query";
}
抽取共享mq配置

我们将mq的配置抽取到nacos中,方便各个微服务共享配置。在nacos中定义一个名为shared-mq.xml的配置文件,内容如下:

spring:
rabbitmq:
host: ${hm.mq.host:192.168.150.101} # 主机名
port: ${hm.mq.port:5672} # 端口
virtual-host: ${hm.mq.vhost:/hmall} # 虚拟主机
username: ${hm.mq.un:hmall} # 用户名
password: ${hm.mq.pw:123} # 密码
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

这里只添加一些基础配置,至于生产者确认,消费者确认配置则由微服务根据业务自己决定。

trade-service模块添加共享配置:

改造下单业务

接下来,我们改造下单业务,在下单完成后,发送延迟消息,查询支付状态。

1)引入依赖在trade-service模块的pom.xml中引入amqp的依赖:

<!--amqp-->
  <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

2)改造下单业务修改trade-service模块的com.hmall.trade.service.impl.OrderServiceImpl类的createOrder方法,添加消息发送的代码:

编写查询支付状态接口

由于MQ消息处理时需要查询支付状态,因此我们要在pay-service模块定义一个这样的接口,并提供对应的FeignClient.首先,在hm-api模块定义三个类:说明:

  • PayOrderDTO:支付单的数据传输实体
  • PayClient:支付系统的Feign客户端
  • PayClientFallback:支付系统的fallback逻辑

PayOrderDTO代码如下:

package com.hmall.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
/**
* <p>
* 支付订单
* </p>
*/
@Data
@ApiModel(description = "支付单数据传输实体")
public class PayOrderDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("业务订单号")
private Long bizOrderNo;
@ApiModelProperty("支付单号")
private Long payOrderNo;
@ApiModelProperty("支付用户id")
private Long bizUserId;
@ApiModelProperty("支付渠道编码")
private String payChannelCode;
@ApiModelProperty("支付金额,单位分")
private Integer amount;
@ApiModelProperty("付类型,1:h5,2:小程序,3:公众号,4:扫码,5:余额支付")
private Integer payType;
@ApiModelProperty("付状态,0:待提交,1:待支付,2:支付超时或取消,3:支付成功")
private Integer status;
@ApiModelProperty("拓展字段,用于传递不同渠道单独处理的字段")
private String expandJson;
@ApiModelProperty("第三方返回业务码")
private String resultCode;
@ApiModelProperty("第三方返回提示信息")
private String resultMsg;
@ApiModelProperty("支付成功时间")
private LocalDateTime paySuccessTime;
@ApiModelProperty("支付超时时间")
private LocalDateTime payOverTime;
@ApiModelProperty("支付二维码链接")
private String qrCodeUrl;
@ApiModelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}

PayClient代码如下:

package com.hmall.api.client;
import com.hmall.api.client.fallback.PayClientFallback;
import com.hmall.api.dto.PayOrderDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "pay-service", fallbackFactory = PayClientFallback.class)
public interface PayClient {
/**
* 根据交易订单id查询支付单
* @param id 业务订单id
* @return 支付单信息
*/
@GetMapping("/pay-orders/biz/{id}")
PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id);
}

PayClientFallback代码如下:

package com.hmall.api.client.fallback;
import com.hmall.api.client.PayClient;
import com.hmall.api.dto.PayOrderDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
@Slf4j
public class PayClientFallback implements FallbackFactory<PayClient> {
  @Override
  public PayClient create(Throwable cause) {
  return new PayClient() {
  @Override
  public PayOrderDTO queryPayOrderByBizOrderNo(Long id) {
  return null;
  }
  };
  }
  }

最后,在pay-service模块的PayController中实现该接口:

@ApiOperation("根据id查询支付单")
@GetMapping("/biz/{id}")
public PayOrderDTO queryPayOrderByBizOrderNo(@PathVariable("id") Long id){
PayOrder payOrder = payOrderService.lambdaQuery().eq(PayOrder::getBizOrderNo, id).one();
return BeanUtils.copyBean(payOrder, PayOrderDTO.class);
}
消息监听

接下来,我们在trader-service编写一个监听器,监听延迟消息,查询订单支付状态:代码如下:

package com.hmall.trade.listener;
import com.hmall.api.client.PayClient;
import com.hmall.api.dto.PayOrderDTO;
import com.hmall.common.domain.MultiDelayMessage;
import com.hmall.trade.constants.MqConstants;
import com.hmall.trade.domain.po.Order;
import com.hmall.trade.service.IOrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderStatusListener {
private final IOrderService orderService;
private final PayClient payClient;
private final RabbitTemplate rabbitTemplate;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = MqConstants.DELAY_ORDER_QUEUE, durable = "true"),
exchange = @Exchange(name = MqConstants.DELAY_EXCHANGE, type = ExchangeTypes.TOPIC),
key = MqConstants.DELAY_ORDER_ROUTING_KEY
))
public void listenOrderCheckDelayMessage(MultiDelayMessage<Long> msg) {
  // 1.获取消息中的订单id
  Long orderId = msg.getData();
  // 2.查询订单,判断状态:1是未支付,大于1则是已支付或已关闭
  Order order = orderService.getById(orderId);
  if (order == null || order.getStatus() > 1) {
  // 订单不存在或交易已经结束,放弃处理
  return;
  }
  // 3.可能是未支付,查询支付服务
  PayOrderDTO payOrder = payClient.queryPayOrderByBizOrderNo(orderId);
  if (payOrder != null && payOrder.getStatus() == 3) {
  // 支付成功,更新订单状态
  orderService.markOrderPaySuccess(orderId);
  return;
  }
  // 4.确定未支付,判断是否还有剩余延迟时间
  if (msg.hasNextDelay()) {
  // 4.1.有延迟时间,需要重发延迟消息,先获取延迟时间的int值
  int delayVal = msg.removeNextDelay().intValue();
  // 4.2.发送延迟消息
  rabbitTemplate.convertAndSend(MqConstants.DELAY_EXCHANGE, MqConstants.DELAY_ORDER_ROUTING_KEY, msg,
  message -> {
  message.getMessageProperties().setDelay(delayVal);
  return message;
  });
  return;
  }
  // 5.没有剩余延迟时间了,说明订单超时未支付,需要取消订单
  orderService.cancelOrder(orderId);
  }
  }
posted on 2025-10-22 13:23  lxjshuju  阅读(7)  评论(0)    收藏  举报