RabbitMQ
消息丢失的源头主要有以下三个:
RabbitMQ
丢失消息下面主要从 3 个方面进行说明并提供应对措施
RabbitMQ
生产者将数据发送到 rabbitmq
的时候,可能数据在网络传输中搞丢了,这个时候 RabbitMQ
收不到消息,消息就丢了。
解决方法:
在生产者发送消息之前,通过 channel.txSelect
开启一个事务,接着发送消息,
RabbitMQ
接收到,生产者会收到异常,此时就可以进行事务回滚 channel.txRollback
然后重新发送;RabbitMQ
收到了这个消息,就可以提交事务 channel.txCommit
;但是这样一来,生产者的吞吐量和性能都会降低很多,现在一般不这么干。
confirm
机制是在生产者设置的,就是每次写消息的时候会分配一个唯一的 id
,然后 RabbitMQ
收到之后会回传一个 ack
,告诉生产者这个消息 ok
了。如果 rabbitmq
没有处理到这个消息,那么就回调一个 nack
的接口,这个时候生产者就可以重发。
事务机制和 confirm
机制最大的不同点:
confirm
机制是异步的,发送一个消息之后就可以发送下一个消息,然后那个消息 rabbitmq
接收了之后会异步回调你一个接口通知你这个消息接收到了;所以一般在生产者这块避免数据丢失,都是用 confirm
机制的。
在 confirm
机制下,我们可以将 channel
设置成 confirm
模式,一旦 channel
进入 confirm
模式,所有在该 channel
上面发布的消息都将会被指派一个唯一的 ID
(从 1 开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ
就会发送一个确认给生产者(包含消息的唯一 ID
),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,RabbitMQ
回传给生产者的确认消息中 delivery-tag
域包含了确认消息的序列号,此外 RabbitMQ
也可以设置 basic.ack
的 multiple
域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm
模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ
因为自身内部错误导致消息丢失,就会发送一条 nack
消息,生产者应用程序同样可以在回调方法中处理该 nack
消息。
confirm
机制和 transaction
事务模式是不能够共存的,已经处于 transaction
事务模式的 channel
不能被设置为 confirm
模式,同理,反过来也一样。
通常我们可以通过调用 channel
的 confirmSelect
方法将 channel
设置为 confirm
模式。如果没有设置 no-wait
标志的话,RabbitMQ
会返回 confirm.select-ok
表示同意生产者当前 channel
信道设置为 confirm
模式。
客户端生产者侧:生产者将消息发送到 RabbitMQ
然后写入到磁盘后通知生成者已收到生产者消息,保证生产者发送的消息不会丢失。
支持两种通知方式:
RabbitMQ
确认后再继续发送消息;RabbitMQ
回应继续发送消息,RabbitMQ
会回调通知生产者是否收到消息,一般实际生产环境用此方式比较多。 import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
public class ConfirmSend {
private static String exchange_name = "";
private static String queue_name = "tx_queue";
/**
* confirm机制:确认publisher发送消息到broker,由broker进行应答(不能确认是否被有效消费)
* confirmSelect,进入confirm消息确认模式
* ,确认方式:1、异步ConfirmListener;2、同步waitForConfirms
* ConfirmListener、waitForConfirms均需要配合confirm机制使用
* @param mes
* @throws Exception
*/
public static void txSend(Serializable mes) throws Exception {
Connection conn = MqManager.newConnection();
Channel channel = conn.createChannel();
// 开启confirm机制
channel.confirmSelect();
channel.queueDeclare(queue_name, false, false, true, null);
// 异步实现发送消息的确认(此部分的消息确认是指发送消息到队列,并非确认消息的有效消费)
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// multiple:测试发现multiple随机true或false,原因未知
System.out.println("Nack deliveryTag:" + deliveryTag + ",multiple:" + multiple);
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Ack deliveryTag:" + deliveryTag + ",multiple:" + multiple);
}
});
for (int i = 0; i < 10; i++) {
System.out.println("---------消息发送-----");
channel.basicPublish(exchange_name, queue_name, null, SerializationUtils.serialize(mes.toString() + i));
}
// channel.waitForConfirms();//同步实现发送消息的确认
System.out.println("-----------");
channel.close();
conn.close();
}
public static void main(String[] args) throws Exception {
txSend("hello world!");
}
}
Confirm
的三种实现方式:
channel.waitForConfirms( )
普通发送方确认模式;channel.waitForConfirmsOrDie( )
批量确认模式;channel.addConfirmListener()
异步监听发送方确认模式;// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(config.UserName);
factory.setPassword(config.Password);
factory.setVirtualHost(config.VHost);
factory.setHost(config.Host);
factory.setPort(config.Port);
Connection conn = factory.newConnection();
// 创建信道
Channel channel = conn.createChannel();
// 声明队列
channel.queueDeclare(config.QueueName, false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}
我们只需要在推送消息之前, channel.confirmSelect( )
声明开启发送方确认模式,再使用 channel.waitForConfirms( )
等待消息被服务器确认即可。
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(config.UserName);
factory.setPassword(config.Password);
factory.setVirtualHost(config.VHost);
factory.setHost(config.Host);
factory.setPort(config.Port);
Connection conn = factory.newConnection();
// 创建信道
Channel channel = conn.createChannel();
// 声明队列
channel.queueDeclare(config.QueueName, false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));
}
channel.waitForConfirmsOrDie(); //直到所有信息都发布,只要有一个未确认就会IOException
System.out.println("全部执行完成");
使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未被确认就会抛出 IOException
异常。
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(config.UserName);
factory.setPassword(config.Password);
factory.setVirtualHost(config.VHost);
factory.setHost(config.Host);
factory.setPort(config.Port);
Connection conn = factory.newConnection();
// 创建信道
Channel channel = conn.createChannel();
// 声明队列
channel.queueDeclare(config.QueueName, false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
String message = String.format("时间 => %s", new Date().getTime());
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));
}
//异步监听确认和未确认的消息
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("未确认消息,标识:" + deliveryTag);
}
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println(String.format("已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
}
});
异步模式的优点,就是执行效率高,不需要等待消息执行完,只需要监听消息即可。
可以看出,代码是异步执行的,消息确认有可能是批量确认的,是否批量确认在于返回的 multiple
的参数,此参数为 bool
值,如果 true
表示批量执行了 deliveryTag
这个值以前的所有消息,如果为 false
的话表示单条确认。
综合总体测试情况来看:Confirm
批量确定和 Confirm
异步模式性能相差不大,Confirm
模式要比事务快 10 倍左右。
RabbitMQ
集群也会弄丢消息,就是说在消息发送到 RabbitMQ
之后,默认是没有保存到磁盘的,万一 RabbitMQ
宕机了,这个时候消息就丢失了。
所以为了解决这个问题,RabbitMQ
提供了一个持久化的机制,消息写入之后会持久化到磁盘,哪怕是宕机了,恢复之后也会自动恢复之前存储的数据,这样的机制可以确保消息不会丢失。
设置持久化步骤:
queue
的时候将其设置为持久化的,这样就可以保证 rabbitmq
持久化 queue
的元数据,但是不会持久化 queue
里的数据;deliveryMode
设置为 2,就是将消息设置为持久化的,此时 rabbitmq
就会将消息持久化到磁盘上去。但是这样一来可能会有人说:万一消息发送到 RabbitMQ
之后,还没来得及持久化到磁盘就挂掉了,数据也丢失了。
对于这个问题,其实是配合上面的 confirm
机制一起来保证的,就是在消息持久化到磁盘之后才会给生产者发送 ack
消息。
RabbitMQ
主要是采用持久化的方式保证消息不丢,启用队列、交换机、消息的持久化,确保不会因为 RabbitMQ
服务器的宕机导致消息丢失。
channel.basicPublish(exchange_name, "routingKey",true, MessageProperties.PERSISTENT_BASIC, "xiao ming".getBytes());
MessageProperties.PERSISTENT_BASIC 即可表示消息是要进行持久化的。
消息持久化成功的条件:
durable
设置为 true
,消息持久化,代码:channel.queueDeclare(x, true, false, false, null)
参数 2 设置为 true
持久化;
deliveryMode
设置为 2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENTTEXTPLAIN,x)
参数 3 设置为存储纯文本到磁盘;
四个条件都需要满足。
持久化工作原理:
RabbitMQ
会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,RabbitMQ
会把这条消息标识为等待垃圾回收。
持久化的缺点:
消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量,尽管使用 SSD
硬盘可以使事情得到缓解,但他仍然吸干了 RabbitMQ
的性能,当消息成千上万条要写入磁盘的时候,性能是很低的。
RabbitMQ
消费者在消费消息的时候,刚拿到消息,结果进程挂了,这个时候 RabbitMQ
就会认为你已经消费成功了,这条数据就丢了。
RabbitMQ
提供了一个消息确认的概念:当一个消息从队列中投递给消费者后,消费者会通知一下消息中间件(RabbitMQ
),这个可以是系统自动 autoACK
的也可以由处理消息的应用操作。
当“消息确认”被启用的时候,RabbitMQ
不会完全将消息从队列中删除,直到它收到来自消费者的确认回执(acknowledgement)。
为了解决这个问题,RabbitMQ
提供了 2 种处理模式来解决这个问题:
RabbbitMQ
将消息发送给应用后,消费者端自动回送一个确认消息。(使用 方法:basic.deliver
或 basic.get-ok
)。RabbitMQ
不会完全将消息从队列中删除,直到消费者发送一个确认回执(acknowledgement)后再删除消息。(使用方法:basic.ack
)。在显式确认模式下,消费者可以自由选择什么时候发送确认回执(acknowledgement)。消费者可以在收到消息后立即发送,或将未处理的消息存储后发送,或等到消息被处理完毕后再发送确认回执。
如果一个消费者在尚未发送确认回执的情况下挂掉了,那 RabbitMQ
会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。
消费者在获取队列消息时,可以指定 autoAck
参数,采用显式确认模式,需要指定 autoAck = false
,在显式确认模式,RabbitMQ
不会为未 ack
的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。如果断开连接,RabbitMQ
也没有收到 ACK
,则 RabbitMQ
会安排该消息重新进入队列,等待投递给下一个消费者。
但是默认情况下这个发送 ack
的操作是自动提交的,也就是说消费者一收到这个消息就会自动返回 ack
给 RabbitMQ
,所以会出现丢消息的问题。
所以针对这个问题的解决方案就是:关闭 RabbitMQ
消费者的自动提交 ack
,在消费者处理完这条消息之后再手动提交 ack
。
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
public class ConsumerTest {
private static String queue_name = "tx_queue";
/**
* @param args
*/
public static void main(String[] args) {
Connection conn;
try {
conn = MqManager.newConnection();
Channel channel = conn.createChannel();
// 消费消息
boolean autoAck = false;
channel.basicConsume(queue_name, autoAck, "myConsumer Tag", new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException {
String routingKey = envelope.getRoutingKey();
String convernType = properties.getContentType();
long deliveryTag = envelope.getDeliveryTag();
System.out.println("routingKey:"+routingKey+",convernType:"+convernType+",deliveryTag:"+deliveryTag+",Msg body:"+new String(body));
channel.basicAck(deliveryTag, false);
}
});
}catch (Exception e) {
e.printStackTrace();
}
}
}
在该流程中,一个分布式事务由 A 和 B 两个服务共同完成,在 A 和 B 都执行成功时,分布式事务结果不会出现以外,但是如果该流程中某一个步骤出现问题,很可能就会导致 AB 的数据不一致的问题。接下来,我们仔细分析一下该流程中的问题所在。
在 A 服务中,由于是本地事务控制,可以保证 a、b 操作的原子性。这里要特别说明的是 b 操作所涉及到的内容:
待这一系列操作完成后,A 认为所有操作完成,提交事务保存。
在 B 服务中,由于是本地事务控制,可以保证 c、d 操作的原子性。这里要特别说明的是 c 操作所涉及到的内容:
本章节部分参考
https://gitbook.cn/books/5d65124b2b27dd24ed390665/index.html
https://gitbook.cn/books/5f30bfb0be80f0592e70f206/index.html