消息丢失
[服务器-持久化]
将内存的数据(交换器 Exchange、队列 Queue、消息 Message)存储到硬盘中,防止 RabbitMQ 宕机导致数据丢失。
[生产者-发布确认]
[消费者-消息应答]
消费者完成一个任务需要一段时间,如果其中一个消费者处理一个长任务并且仅仅完成了部分,此时它宕机了,那么我们将丢失正在处理的消息,以及后续发送给该消费者的消息。
为了保证消息在发送过程中不丢失,RabbitMQ 引入了消息应答机制:消费者处理完成该消息后通知 RabbitMQ,RabbitMQ 才可以删除该消息。
消息应答分为手动应答和自动应答。
[手动应答](推荐)
boolean autoAck = false;
将消息发送给消费者并且等待处理完成后,才会删除队列中的消息。
如果消费者由于某些原因导致消息未发送 ACK 确认,RabbitMQ 会将消息放回队列,将其重新分发给其他消费者处理,确保不会丢失任何消息。
手动应答支持批量应答(multiple),但不推荐使用。
public class Consumer {
channel.basicAck(long deliveryTag, boolean multiple)
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)
channel.basicReject(long deliveryTag, boolean requeue)
[自动应答]
boolean autoAck = true;
只要将消息发送给消费者就删除队列中的消息,不管是否处理完成。
默认采用自动应答,所以要想实现消息不丢失,需要改为手动应答。
消息堆积
消息被拒绝后不再重新入队,而是使用死信队列接收。
消息幂等性
https://help.aliyun.com/document_detail/177412.html
在互联网应用中,尤其在网络不稳定的情况下,消息队列 RabbitMQ 版的消息有可能会出现重复。
1、发送时消息重复
当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。
如果此时 Producer 意识到消息发送失败并尝试再次发送消息,Consumer 后续会收到两条内容相同并且 Message ID 也相同的消息。
2、投递时消息重复
消息消费的场景下,消息已投递到 Consumer 并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。
为了保证消息至少被消费一次,消息队列 RabbitMQ 版的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,Consumer 后续会收到两条内容相同并且 Message ID 也相同的消息。
3、负载均衡时消息重复(包括但不限于网络抖动、服务端重启以及 Consumer 应用重启)
当消息队列 RabbitMQ 版的服务端或客户端重启、扩容或缩容时,会触发 Rebalance,此时 Consumer 可能会收到重复消息。
setnx
消息顺序性
![image]()
对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息,如消费者 A 执行了增加,消费者 B 执行了修改,消费者 C 执行了删除,但是消费者 C 执行比消费者 B 快,消费者 B 又比消费者 A 快,就会导致消费 binlog 执行到数据库的时候顺序错乱,本该顺序是增加、修改、删除,变成了删除、修改、增加。
![image]()
RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。解决这个问题,我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,生产者发送消息的时候,同一个订单号的消息发送到同一个 queue 中,由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。
死信队列
死信,即无法消费的消息。
通常来源于:
1、消息过期
messageProperties.setExpiration("5000");
2、消息超出队列长度
maxLength(5)
3、消息被拒绝
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
延迟队列
=====
下载安装
docker pull docker.io/rabbitmq:3.8-management
docker run -d -it \
--name rabbitmq5672 \
--restart=always \
--privileged=true \
-p 15672:15672 \
-p 5672:5672 \
docker.io/rabbitmq:3.8-management
docker exec -it rabbitmq5672 /bin/bash
rabbitmqctl add_user root rabbitmq
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"
rabbitmqctl set_user_tags root administrator
exit
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.8.0/rabbitmq_delayed_message_exchange-3.8.0.ez
docker cp rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq5672:/opt/rabbitmq/plugins/
rm -rf rabbitmq_delayed_message_exchange-3.8.0.ez
docker exec -it rabbitmq5672 /bin/bash
cd /opt/rabbitmq/plugins/
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
exit
docker restart rabbitmq5672
访问 http://vmwarehost:15672/
账号 root 密码 rabbitmq
综合案例
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: vmwarehost
port: 5672
# addresses: localhost:5672
username: root
password: rabbitmq
# virtual-host: /test
listener:
simple:
# 消息应答
acknowledge-mode: manual
default-requeue-rejected: false
direct:
# 消息应答
acknowledge-mode: manual
default-requeue-rejected: false
# 发布确认
template:
mandatory: true
publisher-confirm-type: correlated
publisher-returns: true
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 配置
*/
@Configuration
public class RabbitMQConfig {
/**
* 主题交换机 01
*/
@Bean("exchange01")
public TopicExchange exchange01() {
return ExchangeBuilder
// Topic
.topicExchange("exchange01")
// 持久化
.durable(true)
.build();
}
/**
* 死信交换机
*/
@Bean("exchange_dead")
public TopicExchange exchangeDead() {
return ExchangeBuilder
// Topic
.topicExchange("exchange_dead")
// 持久化
.durable(true)
.build();
}
/**
* 延迟交换机
*/
@Bean("exchange_delayed")
public CustomExchange exchangeDelayed() {
Map<String, Object> arguments = new HashMap<>(1);
// Topic
arguments.put("x-delayed-type", "topic");
// String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new CustomExchange("exchange_delayed", "x-delayed-message", true, false, arguments);
}
/**
* 普通队列 01
*/
@Bean("queue01")
public Queue queue01() {
return QueueBuilder
// 持久化
.durable("queue01")
.deadLetterExchange("exchange_dead")
.deadLetterRoutingKey("dead")
.maxLength(5)
.build();
}
/**
* 普通队列 02
*/
@Bean("queue02")
public Queue queue02() {
return QueueBuilder
// 持久化
.durable("queue02")
.deadLetterExchange("exchange_dead")
.deadLetterRoutingKey("dead")
.maxLength(5)
.build();
}
/**
* 死信队列
*/
@Bean("queue_dead")
public Queue queueDead() {
return QueueBuilder
// 持久化
.durable("queue_dead")
.build();
}
/**
* 延迟队列
*/
@Bean("queue_delayed")
public Queue queueDelayed() {
return QueueBuilder
// 持久化
.durable("queue_delayed")
.deadLetterExchange("exchange_dead")
.deadLetterRoutingKey("dead")
.maxLength(5)
.build();
}
@Bean
public Binding bindingQueue01AndExchange01(@Qualifier("queue01") Queue queue, @Qualifier("exchange01") TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("*.abc");
}
@Bean
public Binding bindingQueue02AndExchange01(@Qualifier("queue02") Queue queue, @Qualifier("exchange01") TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("abc.#");
}
@Bean
public Binding bindingQueueDeadAndExchangeDead(@Qualifier("queue_dead") Queue queue, @Qualifier("exchange_dead") TopicExchange topicExchange) {
return BindingBuilder.bind(queue).to(topicExchange).with("dead");
}
@Bean
public Binding bindingQueueDelayedAndExchangeDelayed(@Qualifier("queue_delayed") Queue queue, @Qualifier("exchange_delayed") CustomExchange customExchange) {
return BindingBuilder.bind(queue).to(customExchange).with("delayed").noargs();
}
/**
* spring-boot-starter-amqp 默认 Message 的 durable 是 true
* AMQP.BasicProperties props = MessageProperties.PERSISTENT_TEXT_PLAIN;
* channel.basicPublish(exchange, routingKey, props, body);
*/
}
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
/**
* @author 生产者
*/
@RestController
@RequestMapping("/producer")
public class Producer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
@Resource
private RabbitTemplate rabbitTemplate;
@GetMapping("/send/{msg}")
public void send(@PathVariable String msg) {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
rabbitTemplate.convertAndSend("exchange01", "1.abc", msg.getBytes(StandardCharsets.UTF_8),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
// 设置过期
messageProperties.setExpiration("5000");
return message;
}
}
);
rabbitTemplate.convertAndSend("exchange01", "abc.123", msg.getBytes(StandardCharsets.UTF_8),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
// 设置过期
messageProperties.setExpiration("5000");
return message;
}
}
);
rabbitTemplate.convertAndSend("exchange_delayed", "delayed", msg.getBytes(StandardCharsets.UTF_8),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
// 设置延迟
messageProperties.setDelay(10000);
return message;
}
}
);
rabbitTemplate.convertAndSend("exchange_delayed", "delayed", msg.getBytes(StandardCharsets.UTF_8),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
// 设置延迟
messageProperties.setDelay(5000);
return message;
}
}
);
System.out.println(LocalDateTime.now() + " 发送消息: " + msg);
}
/**
* 1、消息发布到交换机失败,后续自然无法发布到队列。[触发回调]
* 2、消息发布到交换机成功,但是发布到队列失败。[触发回调]
* 3、消息发布到交换机和队列均成功。[触发回调]
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
final String prefix = "[生产者] 发布到 [交换机] 的确认";
System.out.println(prefix + " correlationData: " + correlationData);
System.out.println(prefix + " ack: " + ack);
System.out.println(prefix + " cause: " + cause);
// TODO 将丢失的消息存储至 Redis,随后开启定时任务重新发布消息
}
/**
* 1、消息发布到交换机失败,后续自然无法发布到队列。
* 2、消息发布到交换机成功,但是发布到队列失败。[触发回调]
* 3、消息发布到交换机和队列均成功。
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
final String prefix = "[交换机] 发布到 [队列] 的确认";
System.out.println(prefix + " message: " + new String(message.getBody(), StandardCharsets.UTF_8));
System.out.println(prefix + " replyCode: " + replyCode);
System.out.println(prefix + " replyText: " + replyText);
System.out.println(prefix + " exchange: " + exchange);
System.out.println(prefix + " routingKey: " + routingKey);
// TODO 将丢失的消息存储至 Redis,随后开启定时任务重新发布消息
}
}
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
/**
* @author 消费者
*/
@Component
public class Consumer {
@RabbitListener(queues = "queue01")
public void handleMessageFromQueue01(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
if ("abc".equals(msg)) {
// 应答信息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
System.out.println("接收 queue01 消息: " + msg);
} else {
// 拒绝信息,不再重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
@RabbitListener(queues = "queue02")
public void handleMessageFromQueue02(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
if (!"abc".equals(msg)) {
// 应答信息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
System.out.println("接收 queue02 消息: " + msg);
} else {
// 拒绝信息,不再重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
@RabbitListener(queues = "queue_dead")
public void handleMessageFromQueueDead(Message message, Channel channel) throws IOException {
// 应答信息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println("接收 queue_dead 消息: " + msg);
}
@RabbitListener(queues = "queue_delayed")
public void handleMessageFromQueueDelayed(Message message, Channel channel) throws IOException {
// 应答信息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println(LocalDateTime.now() + " 接收 queue_delayed 消息: " + msg);
}
}