RabbitMQ 的高级特性浅尝
消息的可靠投递
作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我哦哦们提供了两种方式用来控制消息的投递可靠性模式:
- confirm 确认模式
- return 退回模式
rabbitmq 整个消息投递的路径为:producer --> rabbitmq broker --> exchange --> queue --> consumer
- 消息从
produce 到 exchange 成功
则会返回一个confirmCallback 。 - 消息从
exchange --> queue投递失败
则会返回一个returnCallback。
我们可以利用这两个callback 控制消息的可靠性投递。
- 配置中开启确认模式
spring:
rabbitmq:
addresses: 127.0.0.1 # mq ip地址
host: 5672 # 默认为5672
username: guest # 默认为guest
password: guest # 默认为guest
virtual-host: /mohen # 默认为/
publisher-confirm-type: correlated # confirm 模式
publisher-returns: true # return 模式
# NONE值是禁用发布确认模式,是默认值
# CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
# SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
- 在rabbitTemplate 定义回调函数
// ConfirmCallBack
@Test
void confirmQueue() {
/**
correlationData: 相关配置信息
ack: 交换机是否成功收到消息。true 成功,false 失败
cause: 失败原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
System.out.println("消息发送成功!");
}else {
System.out.println("消息发送失败!");
}
});
rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_CONFIRM_EXCHANGE_NAME02, "confirm", "confirm");
}
// ReturnsCallback
@Test
void returnQueue() {
rabbitTemplate.setReturnsCallback(returned -> {
System.out.println("消息发送失败");
System.out.println("Message: " + returned.getMessage());
System.out.println("ReplyCode: " + returned.getReplyCode());
System.out.println("ReplyText: " + returned.getReplyText());
System.out.println("Exchange: " + returned.getExchange());
System.out.println("RoutingKey: " + returned.getRoutingKey());
});
rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_CONFIRM_EXCHANGE_NAME02, "confirm", "confirm");
}
Consumer Ack
ack 指Acknowlege 确认,表示消费端收到消息后的确认方式。
有三种模式:
- 自动确认:acknowledge:
- 手动确认:acknowledge:
- 根据异常情况确认:acknowledge:
自动确认是指:当消息 一旦被Consumer 接收到,就自动确认,并将相应的message 从队列的缓存中移除。但是实际业务中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
手动确认:自己在业务处理成功后,手动调用channel.basicAck() 手动签收,如果出现异常则调用channel.basicNack() 方法,让其自动重新发送消息。
- 在消费端配置中开启手动模式
spring:
rabbitmq:
addresses: 127.0.0.1 # mq ip地址
host: 5672 # 默认为5672
username: guest # 默认为guest
password: guest # 默认为guest
virtual-host: /mohen # 默认为/
listener:
simple:
acknowledge-mode: manual # 开启手动确认
- 生产端发送消息
- 消费端开启监听
@Component
public class AckListener {
@RabbitListener(queues = "confirm_queue")
public void ackListener(Message message, Channel channel) throws InterruptedException {
Thread.sleep(2000);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1.接收转换消息
System.out.println(new String(message.getBody()));
//2.处理业务逻辑
System.out.println("处理业务逻辑!");
//3.若没有异常则进行消息签收
int i = 3 / 0;
/**
* basicAck(long deliveryTag, boolean multiple)
* 参数:
* deliveryTag:收到的消息的标签
* multiple:是否签收所有消息
*/
channel.basicAck(deliveryTag, false);
//模拟异常
System.out.println("已签收消息!");
} catch (Exception e) {
//出现异常,拒收消息
/**
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* 参数:
* deliveryTag:收到的消息的标签
* multiple:是否签收所有消息
* requeue:重回队列。若设置为true,则消息会重新回到队列,broker会重新发送该消息给消费端
*/
try {
System.out.println("已拒收消息!");
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
消费端限流
配置中开启限流,前提条件保证开启手动确认。其他地方同上即可
spring:
rabbitmq:
# addresses: 127.0.0.1 # mq 地址
# host: 5672
# username: guest # 默认为guest
# password: guest # 默认为guest
virtual-host: /mohen # 默认为/
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 开启手动确认
prefetch: 2 # 开启限流,每次手动签收后,再处理下一批
TTL
ttl:Time to Live 存活时间,当消息到达存活时间后,还没有被消费会被自动清除。
-
RabbitMQ 可以对单个消息设置过期时间,也可以对整个队列设置过期时间。
-
创建队列时,通过
x-message-ttl
参数统一设置队列过期时间,单位:ms。 -
消息的过期时间通过
expiration
参数设置,单位:ms。 -
如果两者都设置了,以时间短的为准,队列过期后,会将队列中所有的消息全部移除。
-
消息过期后,不会立即将其移除,只有在队列顶端时,才会判断其是否过期。
public static final String TTL_QUEUE_NAME = "ttl_queue";
// 队列过期时间设置
@Bean(TTL_QUEUE_NAME)
public Queue ttlQueue() {
// x-message-ttl: 最大存活时间
return QueueBuilder.durable(TTL_QUEUE_NAME).withArgument("x-message-ttl",20000).build();
}
// 消息过期时间设置
@Test
void ttlQueue() {
// 1. 统一设置过期时间
rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE_NAME, "ttl.chsj", "ttl-ty");
// 2. 单独设置过期时间
rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE_NAME, "ttl.chsj", "ttl-dd", message -> {
// 1.设置message 的信息过期时间
message.getMessageProperties().setExpiration("5000");
// 2. 返回该消息
return message;
});
}
死信队列
dlx: Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另外一个交换机,这个交换机就是dlx,与普通消息没有太大区别。
消息成为死信的三种情况:
-
队列消息长度到达限制
-
消费者拒收,basicNack/basicReject 并且不把消息重新放入原目标队列
有一种场景需要注意下:消费者设置了自动
ACK
,当重复投递次数达到了设置的最大retry
次数之后,消息也会投递到死信队列,但是内部的原理还是调用了basicNack()
或basicReject()
(这种情况我没写例子)
这个帖子写的比较详细: 死信队列,点击查看 -
原队列存在消息过期设置,消息到达超时时间未被消费
队列绑定死信交换机:
给正常队列设置参数: x-dead-letter-exchange 死信交换机名称 和 x-dead-letter-routing-key 发送给死信交换机的路由键
分别来展示这几种情况,首先定义死信队列和正常队列
// 死信交换机
public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
// 正常
public static final String UNDLX_EXCHANGE_NAME = "undlx_exchange";
// 死信
public static final String DLX_QUEUE_NAME = "dlx_queue";
// 正常
public static final String UNDLX_QUEUE_NAME = "undlx_queue";
public static final String UNDLX_QUEUE_NAME02 = "undlx_queue02";
// 定义交换机
@Bean(DLX_EXCHANGE_NAME)
public Exchange dlxExchange() {
return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).durable(true).build();
}
@Bean(UNDLX_EXCHANGE_NAME)
public Exchange undlxExchange() {
return ExchangeBuilder.topicExchange(UNDLX_EXCHANGE_NAME).durable(true).build()
}
// 定义队列
@Bean(DLX_QUEUE_NAME)
public Queue dlxQueue() {
return QueueBuilder.durable(DLX_QUEUE_NAME).build();
}
@Bean(UNDLX_QUEUE_NAME)
public Queue undlxQueue() {
Map<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
map.put("x-dead-letter-routing-key", "dlx.hhh");
return QueueBuilder.durable(UNDLX_QUEUE_NAME).withArguments(map).build();
}
@Bean(UNDLX_QUEUE_NAME02)
public Queue undlx02Queue() {
Map<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
map.put("x-dead-letter-routing-key", "dlx.hhh");
//x-max-length: 队列最大长度
map.put("x-max-length", 2);
return QueueBuilder.durable(UNDLX_QUEUE_NAME02).withArguments(map).build();
}
//绑定交换机与队列
@Bean
public Binding bindQueueExchange09(@Qualifier(DLX_QUEUE_NAME) Queue queue, @Qualifier(DLX_EXCHANGE_NAME) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
@Bean
public Binding bindQueueExchange10(@Qualifier(UNDLX_QUEUE_NAME) Queue queue, @Qualifier(UNDLX_EXCHANGE_NAME) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("undlx.#").noargs();
}
@Bean
public Binding bindQueueExchange11(@Qualifier(UNDLX_QUEUE_NAME02) Queue queue, @Qualifier(UNDLX_EXCHANGE_NAME) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("undlx02.#").noargs();
}
生产端发送消息,代码我写在一起了,可以分别放开注释进行测试
@Test
void dlxQueue() {
// 1. 消费端拒收
// rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx.chsj", "undlx-nack");
// 2. 队列长度限制
/*for (int i = 5; i > 0; i--) {
rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx02.chsj", "undlx-maxLength");
}*/
// 3. 过期时间,测试这个时,消费端不启动,在管理控制台可以看到消息从正常队列转到死信队列中
/*rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx.chsj", "undlx-ttl", message -> {
// 1. 设置message 的信息过期时间
message.getMessageProperties().setExpiration("5000");
// 2. 返回该消息
return message;
});*/
}
消费端也写在一起了
@Component
public class DlxListener {
// @RabbitListener(queues = "undlx_queue")
@RabbitListener(queues = "undlx_queue02") // 用于测试长度限制
public void undlxListener(Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
Thread.sleep(2000);
System.out.println("正常-接收: " + new String(message.getBody()));
channel.basicAck(deliveryTag, true);
// System.out.println("正常-拒收,并不重回队列: " + new String(message.getBody()));
// channel.basicNack(deliveryTag, true, false); // 用于测试拒收
} catch (Exception ex) {
ex.printStackTrace();
}
}
@RabbitListener(queues = "dlx_queue")
public void dlxListener(Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println("死信-接收: " + new String(message.getBody()));
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
常见需求:下单后,30 分钟未付款,订单自动取消。
实现方式:1. 定时器 2. 延迟队列
但是RabbitMQ 中并没有直接提供延迟队列的功能,不过可以使用TTL+DLX组合的方式来实现延迟队列的效果.
模拟上述需求,代码与进入死信队列的第三种情况类似,这里我也写一下吧
// 配置信息
public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
public static final String DELAY_EXCHANGE_NAME = "delay_exchange";
public static final String DLX_QUEUE_NAME = "dlx_queue";
public static final String DELAY_QUEUE_NAME = "delay_queue";
@Bean(DLX_EXCHANGE_NAME)
public Exchange dlxExchange() {
return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).durable(true).build();
}
@Bean(DELAY_EXCHANGE_NAME)
public Exchange delayExchange() {
return ExchangeBuilder.topicExchange(DELAY_EXCHANGE_NAME).durable(true).build();
}
@Bean(DLX_QUEUE_NAME)
public Queue dlxQueue() {
return QueueBuilder.durable(DLX_QUEUE_NAME).build();
}
@Bean(DELAY_QUEUE_NAME)
public Queue delayQueue() {
Map<String, Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
map.put("x-dead-letter-routing-key", "dlx.delay");
map.put("x-message-ttl", 10000); // 10s
return QueueBuilder.durable(DELAY_QUEUE_NAME).withArguments(map).build();
}
@Bean
public Binding bindQueueExchange09(@Qualifier(DLX_QUEUE_NAME) Queue queue, @Qualifier(DLX_EXCHANGE_NAME) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
@Bean
public Binding bindQueueExchange12(@Qualifier(DELAY_QUEUE_NAME) Queue queue, @Qualifier(DELAY_EXCHANGE_NAME) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("#.delay").noargs();
}
// 模拟调用订单系统
@Test
void delayQueue() {
rabbitTemplate.convertAndSend(RabbitConfig.DELAY_EXCHANGE_NAME, "order.delay", "order-delay");
}
//消费端接收
@Component
public class DalayListener {
// 实现延迟队列效果,需要监听的是死信队列
@RabbitListener(queues = "dlx_queue")
public void dlxListener(Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.println("死信-接收: " + new String(message.getBody()) + "开始实现业务逻辑...");
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
日志与监控
日志
在Windows 上的日志文件在 C:\Users\用户名\AppData\Roaming\RabbitMQ\log/rabbit@主机名.log
Linux:/var/log/rabbitmq/rabbit@主机名.log
日志包含了Eralng 的版本号,RabbitMQ 的版本号、服务节点名称,cookie的hash值等等信息,有利于异常情况发生后排查问题。
管理控制台
可以监控rabbitmq 使用的连接,信道,交换机,队列及用户的情况。
linux 上可以通过命令的形式进行查看