一:🐱🏍问题引入
前面提到可以使用RabbitMQ实现订单到期自动取消以及当超过某一时间订单还是显示未支付时候就可以通过延迟队列主动向微信支付后台进行订单查询。
由于RabbitMQ是基于Erlang语言开发的,因此要使用RabbitMQ,首先要安装Erlang,至于安装教程可以自行百度解决,然后就是安装RabbitMQ并进行相关配置。
在RabbitMQ 3.6.X之前,要实现延迟队列只能通过TTL(生存时间)+ DLX(死信交换机)来实现,实现过程并不复杂。在RabbitMQ官方文档中有这样一句话:Dead letter exchanges (DLXs) are normal exchanges. They can be any of the usual types and are declared as usual. 意思是死信交换机是一个普通的交换机,它可以被当做普通交换机来使用。关键点在于这个交换机是用来存放过期消息的,所以这一交换机就称为死信交换机,流程图见下图:
设置过期时间有两种方法,一种是单独针对每一条消息进行设置,但是这样会因为时序问题形成队头阻塞现象。因为队列消息是按序消费的,如果队头的消息延迟时间是 10s, 后面的消息都要等至少 10s 后才可以进行消费。另一种方法是设置过期时间在消息队列上,如果过期时间设置在队列上,所有发送到队列的消息延迟时间都是该队列设定值,而业务需求延迟时间是随着重试次数线性增长的,这样就需要创建很多个固定延迟时间的队列。
可以看到无论采用哪一种方式都有很大的缺陷,但是在这个项目中是可以采用第二种方式的,因为针对每一笔订单设置的过期时间都为5分钟。
在RabbitMQ 3.6.X之后,RabbitMQ推出了delay-message 插件,该插件可以更好地实现延迟队列,当然,要使用这个插件还需要自行进行安装,具体安装过程可以自己百度解决。使用该插件的好处有两个方面,当然就是针对上面两种方案的缺陷来改进的。
首先,它是将延迟时间设置在消息上的,这样只要创建一个队列即可;
其次,指定为延迟类型的交换机在接收到消息后并未立即将消息投递至目标队列中,而是存储在 mnesia (一个分布式数据系统)表中,检测消息延迟时间,在达到可投递时间时才投递至目标队列,这样就不存在队头阻塞现象。
二:🐱🏍相关插件
登录web页面查看RabbitMQ的版本 (可以向下兼容)
插件下载地址:https://www.rabbitmq.com/community-plugins.html
找到rabbitmq_delayed_message_exchange下载
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
将插件复制到RabbitMQ容器内,进入容器安装插件
docker cp rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez rabbitmq容器ID:/plugins
docker exec -it rabbitmq容器ID bash
chmod 777 /plugins/rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
验证是否成功, Exchanges的type多了一个x-delayed-message
三:🐱🏍相关配置
rabbitmq:
host: 1.1.1.1
port: 5672
username: admin
password: admin
# 设置虚拟主机
virtual-host: delay-order
connection-timeout: 15000
# 用来配置消息发送到交换器之后是否触发回调方法
publisher-confirm-type: correlated
# 触发路由失败消息的回调
publisher-returns: true
template:
# 必须设置成true 消息路由失败通知监听者,而不是将消息丢弃
mandatory: true
listener:
simple:
# 每次从RabbitMQ获取的消息数量(限流)
prefetch: 1
default-requeue-rejected: false
# 每个队列启动的消费者数量
concurrency: 1
# 每个队列最大的消费者数量
max-concurrency: 1
# 手动确认
acknowledge-mode: manual
需要说明的是,publisher-confirm-type设置为correlated表示消息发送到交换机之后会发送回调通知给生产者,如果由于RabbitMQ内部原因导致交换机接收失败返回失败回调信息之后需要进行异常处理。publisher-returns这一参数实质上是用不上的,因为延时消息是从磁盘上读取消息然后发送(后台任务),发送消息时候无法保证两点:
发送时消息路由队列还存在
发送时原连接仍然支持回调方法
因为消息写磁盘和读磁盘消息发送存在时间差,两个时间点的队列和连接情况可能不同,所以不支持Mandatory设置。(publisher-returns: true必须与template.mandatory: true一起设置路由失败消息的回调才能生效)。
此外,为了保证消息传递的可靠性,我将消息确认机制设置为手动确认,同时每次只能过来一条数据。
四:🐱🏍代码实现
4.1:RabbitmqConfig(配置类)
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author My.Peng
*/
@Configuration
public class RabbitmqConfig {
/**
* 交换机名称
*/
public static final String DELAY_EXCHANGE_NAME = "plugin.delay.exchange";
//消息队列名称 消费者
/**
* 支付成功队列
*/
public static final String DELAY_QUEUE_ORDER_SUCCESS_NAME = "plugin.delay.success.order.queue";
/**
* 订单超时/取消处理队列
*/
public static final String DELAY_QUEUE_ORDER_NAME = "plugin.delay.order.queue";
/**
* 退款处理队列
*/
public static final String DELAY_QUEUE_REFUND_NAME = "plugin.delay.refund.queue";
//路由名称 生产者
/**
* 支付成功队列
*/
public static final String ROUTING_KEY_ORDER_SUCCESS = "plugin.delay.success.routing_order";
/**
* 订单超时/取消处理队列
*/
public static final String ROUTING_KEY_ORDER = "plugin.delay.routing_order";
/**
* 退款路由名称
*/
public static final String ROUTING_KEY_REFUND = "plugin.delay.routing_refund";
/**
* 声明一个交换机
*
* @return
*/
@Bean("DELAY_EXCHANGE")
CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
/**
* 声明一个支付成功延迟队列
*/
@Bean("DELAY_QUEUE_ORDER_SUCCESS_NAME")
Queue orderSuccessDelayQueue() {
return QueueBuilder.durable(DELAY_QUEUE_ORDER_SUCCESS_NAME).build();
}
/**
* 声明一个订单延迟队列
*
* @return
*/
@Bean("ORDER_DELAY_QUEUE")
Queue orderDelayQueue() {
return QueueBuilder.durable(DELAY_QUEUE_ORDER_NAME).build();
}
/**
* 声明一个退款延迟队列
*
* @return
*/
@Bean("REFUND_DELAY_QUEUE")
Queue refundDelayQueue() {
return QueueBuilder.durable(DELAY_QUEUE_REFUND_NAME).build();
}
/**
* 订单支付成功延迟队列绑定
*
* @param orderSuccessDelayQueue
* @param delayExchange
* @return
*/
@Bean
Binding orderSuccessDelayQueueBinding(@Qualifier("DELAY_QUEUE_ORDER_SUCCESS_NAME") Queue orderSuccessDelayQueue, @Qualifier("DELAY_EXCHANGE") CustomExchange delayExchange) {
return BindingBuilder.bind(orderSuccessDelayQueue).to(delayExchange).with(ROUTING_KEY_ORDER_SUCCESS).noargs();
}
/**
* 订单超时/取消延迟队列绑定
*
* @param orderDelayQueue
* @param delayExchange
* @return
*/
@Bean
Binding orderDelayQueueBinding(@Qualifier("ORDER_DELAY_QUEUE") Queue orderDelayQueue, @Qualifier("DELAY_EXCHANGE") CustomExchange delayExchange) {
return BindingBuilder.bind(orderDelayQueue).to(delayExchange).with(ROUTING_KEY_ORDER).noargs();
}
/**
* 订单退款延迟队列绑定
*
* @param refundDelayQueue
* @param delayExchange
* @return
*/
@Bean
Binding refundDelayQueueBinding(@Qualifier("REFUND_DELAY_QUEUE") Queue refundDelayQueue, @Qualifier("DELAY_EXCHANGE") CustomExchange delayExchange) {
return BindingBuilder.bind(refundDelayQueue).to(delayExchange).with(ROUTING_KEY_REFUND).noargs();
}
}
4.2:RabbitmqDelayProducer(消息生产者)
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author My.Peng
* @description 消息生产者
*/
@Component
@Slf4j
public class RabbitmqDelayProducer {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* @param no 消息
* @param messageId 唯一id
* @param exchangeName 交换机
* @param key 路由键
* @param delayTime 延迟时间(毫秒)
*/
public void publish(String no, String messageId, String exchangeName, String key, Integer delayTime) {
/* 确认的回调 确认消息是否到达 Broker 服务器 其实就是是否到达交换器
* 如果发送时候指定的交换器不存在 ack 就是 false 代表消息不可达
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
assert correlationData != null;
String message_Id = correlationData.getId();
//返回成功,表示消息被正常投递到交换机
if (ack) {
log.info("信息投递到交换机成功,messageId:{}", message_Id);
} else {
log.error("交换机不可达,messageId:{} 原因:{}", message_Id, cause);
}
});
/*
* 延时消息是从磁盘读取消息然后发送(后台任务),发送消息的时候无法保证两点:
*
* 1、发送时消息路由的队列还存在
* 2、发送时原连接仍然支持回调方法
* 原因:消息写磁盘和从磁盘读取消息发送存在时间差,两个时间点的队列和连接情况可能不同。所以不支持Mandatory设置
*
* 消息失败的回调
* 例如消息已经到达交换器上,但路由键匹配任何绑定到该交换器的队列,会触发这个回调,此时 replyText: NO_ROUTE
* 用不上
*/
rabbitTemplate.setMandatory(false);
rabbitTemplate.setReturnsCallback(returnedMessage -> {
String message_Id = returnedMessage.getMessage().getMessageProperties().getMessageId();
byte[] message = returnedMessage.getMessage().getBody();
Integer replyCode = returnedMessage.getReplyCode();
String replyText = returnedMessage.getReplyText();
String exchange = returnedMessage.getExchange();
String routingKey = returnedMessage.getRoutingKey();
log.warn("消息:{} 发送失败,消息ID:{} 应答码:{} 原因:{} 交换机:{} 路由键:{}",
new String(message), message_Id, replyCode, replyText, exchange, routingKey);
}
);
// 在实际中ID 应该是全局唯一 能够唯一标识消息 消息不可达的时候触发ConfirmCallback回调方法时可以获取该值,进行对应的错误处理
CorrelationData correlationData = new CorrelationData(messageId);
rabbitTemplate.convertAndSend(exchangeName, key, no, message -> {
// 设置延迟时间
message.getMessageProperties().setDelay(delayTime);
return message;
}, correlationData);
}
}
4.3:RabbitmqDelayConsumer(消息消费者)
import com.goal.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.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author My.Peng
*/
@Component
@Slf4j
public class RabbitmqDelayConsumer {
//@Resource
//private OrderService orderService;
/**
* 监听订单支付成功延迟队列
*
* @param tradeNo 订单编号
*/
@RabbitListener(queues = {"plugin.delay.success.order.queue"})
public void orderDelaySuccessQueue(String tradeNo, Message message, Channel channel) throws Exception {
log.info("订单支付成功队列 接收订单{}", tradeNo);
try {
//处理订单支付成功消息
//orderService.queryOrderSuccessStatus(tradeNo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("订单{}处理成功", tradeNo);
} catch (Exception e) {
e.printStackTrace();
log.info("订单{}处理失败。进行重新入队等待处理", tradeNo);
//消息重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
/**
* 监听订单取消/超时延迟队列
*
* @param orderNo 订单编号
*/
@RabbitListener(queues = {"plugin.delay.order.queue"})
public void orderDelayQueue(String orderNo, Message message, Channel channel) throws Exception {
log.info("订单取消/超时队列 接收订单{}", orderNo);
try {
//处理订单取消/超时消息
//orderService.checkOrderOffStatus(orderNo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("订单{}处理成功", orderNo);
} catch (Exception e) {
e.printStackTrace();
log.info("订单{}处理失败。进行重新入队等待处理", orderNo);
//消息重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
/**
* 监听退款延迟队列
*
* @param refundNo 退款编号
*/
@RabbitListener(queues = {"plugin.delay.refund.queue"})
public void refundDelayQueue(String refundNo, Message message, Channel channel) throws Exception {
log.info("订单退款队列 接收订单{}", refundNo);
try {
//处理订单退款信息
//orderService.checkRefundStatus(refundNo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info("订单{}退款成功", refundNo);
} catch (Exception e) {
e.printStackTrace();
log.info("订单{}退款失败。进行重新入队等待退款", refundNo);
//消息重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
4.4:测试消息生产者
public static void main(String[] args) {
rabbitmqDelayProducer.publish(orderParam.getOrderNo(),orderParam.getOrderNo(),
RabbitmqConfig.DELAY_EXCHANGE_NAME,RabbitmqConfig.ROUTING_KEY_ORDER, 1000 * 60 * 2);
}