使用 RabbitMQ 实现异步通信的时候,消息丢了怎么办,消息重复消费怎么办?
在 RabbitMQ 里面提供了很多保证消息可靠投递的机制,这个也是 RabbitMQ 的一个特性。
要明确一个问题,因为效率与可靠性是无法兼得的,如果要保证每一个环节都成功,势必会对消息的收发效率造成影响。所以如果是一些业务实时一致性要求不是特别高的场合,可以牺牲一些可靠性来换取效率。
比如发送通知或者记录日志的这种场景,如果用户没有收到通知,不会造成业务影响,只要再次发送就可以了。
在我们使用 RabbitMQ 收发消息的时候,有几个主要环节
①代表消息从生产者发送到 Broker
生产者把消息发到 Broker 之后,怎么知道自己的消息有没有被 Broker 成功接收?
②代表消息从 Exchange 路由到 Queue
Exchange 是一个绑定列表,如果消息没有办法路由到正确的队列,会发生什么事情?应该怎么处理?
③代表消息在 Queue 中存储
队列是一个独立运行的服务,有自己的数据库(Mnesia),它是真正用来存储消息的。如果还没有消费者来消费,那么消息要一直存储在队列里面。如果队列出了问题,消息肯定会丢失。怎么保证消息在队列稳定地存储呢?
④代表消费者订阅 Queue 并消费消息
队列的特性是什么?FIFO。队列里面的消息是一条一条的投递的,也就是说,只有上一条消息被消费者接收以后,才能把这一条消息从数据库删掉,继续投递下一条消息。那么问题来了,Broker 怎么知道消费者已经接收了消息呢?
第一个环节是生产者发送消息到 Broker。可能因为网络或者 Broker 的问题导致消息发送失败,生产者不能确定 Broker 有没有正确的接收。
在 RabbitMQ 里面提供了两种机制服务端确认机制,也就是在生产者发送消息给RabbitMQ 的服务端的时候,服务端会通过某种方式返回一个应答,只要生产者收到了这个应答,就知道消息发送成功了。
第一种是 Transaction(事务)模式,第二种 Confirm(确认)模式。
事务模式怎么使用呢?
我们通过一个 channel.txSelect()的方法把信道设置成事务模式,然后就可以发布消息给 RabbitMQ 了,如果 channel.txCommit();的方法调用成功,就说明事务提交成功,则消息一定到达了 RabbitMQ 中。
如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行 channel.txRollback()方法来实现事务回滚。
在事务模式里面,只有收到了服务端的 Commit-OK 的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干 RabbitMQ 服务器的性能。所以不建议大家在生产环境使用。
Spring Boot 中的设置
rabbitTemplate.setChannelTransacted(true);
/**
* @author yhd
* @createtime 2021/1/24 23:29
* rabbitmq事务
*/
@Component
public class MqTx {
private static final String EXCHANGE = "exchange.tx";
private static final String QUEUE = "queue.tx";
private static final String ROUTING_KEY = "routing.tx";
@Resource
private RabbitTemplate rabbitTemplate;
public boolean sendMessage() {
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setConfirmCallback((correlationData, flag, cause) -> {
if (flag) {
System.out.println("发送成功!");
} else {
System.out.println("发送失败" + cause);
}
});
return true;
}
@RabbitListener(bindings =
@QueueBinding(
value = @Queue(value = QUEUE, autoDelete = "false", durable = "true"),
exchange = @Exchange(value = EXCHANGE, autoDelete = "true", durable = "true"),
key = {
ROUTING_KEY}
)
)
public void receiveMessage(String msg, Message message, Channel channel) {
try {
channel.txSelect();
System.out.println("msg = "