原文链接(作者一个人):https://juejin.im/post/5d468591f265da03b810427e

工作中经常用到消息中间件来解决系统间的解耦问题或者高并发消峰问题,但是消息的可靠性如何保证一直是个很大的问题,什么情况下消息就不见了?如何防止消息丢失?下面通过这篇文章,我们就聊聊RabbitMQ 消息可靠性如何解决的?

本文分三部分说明

  1. RabbitMQ 消息丢失场景有哪些?
  2. 如何避免消息丢失?
  3. 如何设计部署消息中间件保证消息可靠性?

RabbitMQ 消息丢失场景有哪些?

首先我们看下消息周期投递过程:

我们把该图分三部分,左中右,每部分都会导致消息丢失情况,下面就详细聊聊每个阶段消息是如何丢的:

1.生产者生产消息到RabbitMQ Server 消息丢失场景

1) 外界环境问题导致:发生网络丢包、网络故障等造成RabbitMQ Server端收不到消息,因为生产环境的网络是很复杂的,网络抖动,丢包现象很常见,下面会讲到针对这个问题是如何解决的。

2) 代码层面,配置层面,考虑不全导致消息丢失

事例1:
一般情况下,生产者使用Confirm模式投递消息,如果方案不够严谨,比如RabbitMQ Server 接收消息失败后会发送nack消息通知生产者,生产者监听消息失败或者没做任何事情,消息存在丢失风险;

事例2:
生产者发送消息到exchange后,发送的路由和queue没有绑定,消息会存在丢失情况,下面会讲到具体的例子,保证意外情况的发生,即使发生,也在可控范围内。

2.RabbitMQ Server中存储的消息丢失或可靠性不足

1)消息未完全持久化,当机器重启后,消息会全部丢失,甚至Queue也不见了

假如:你仅仅持久化了Message,而Exchange,Queue没有持久化,这个持久化是无效的。 记得之前公司有一哥们忘记持久化Queue导致机器重启后,Queue不见了,自然Message也丢失了。

2)单节点模式问题,如果某个节点挂了,消息就不能用了,业务可能瘫痪,只能等待

如果做了消息持久化方案,消息会持久化硬盘,机器重启后消息不会丢失;但是还有一个极端情况,这台服务器磁盘突然坏了(公司遇到过磁盘问题还是很多的),消息持久化不了,非高可用状态,这个模式生产环境慎重考虑。

3)普通集群模式:某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(建立在消息持久化)

虽然这个模式进步了一点点,多个节点,但是消息还是不能保证可靠,为什么呢?

因为RabbitMQ 集群模式有点特殊,队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据(可以理解为索引,这也是为了提高性能,如果每次把所有内容同步到所有节点是有开销代价的)。 下面自己画了一张图介绍普通集群丢失消息情况:

这里有三个节点,通常情况下一个磁盘节点,两个内存节点,首先先说明下, Queue1 内容仅仅存在节点note1上面,在创建队列的时候已经固定了,note2,note3 仅仅存放的是元数据,这个一定要清楚,Producer发送消息到note2,note2 会同步元数据到其他节点,内容会同步note1。

那我们想下,图中的Q1问题,note1挂了,这个节点的Queues全部暂时不可用,节点恢复后可用。

我们说下图片中备注2中的问题,Producer发送消息到note2,note2在同步note1前note1挂了,此时你的心情是怎么样的。。。后面会讲具体的策略

4)镜像模式:可以解决上面的问题,但是还是有意外情况发生

比如:持久化的消息,保存到硬盘过程中,当前队列节点挂了,存储节点硬盘又坏了,消息丢了,怎么办?下面会详细介绍

3.RabbitMQ Server到消费者消息丢失

  1. 消费端接收到相关消息之后,消费端还没来得及处理消息,消费端机器就宕机了,此时消息如果处理不当会有丢失风险,后面会讲到如何处理这个情况,消费端也有ack机制

如何避免消息丢失?

下面也是从三个方面介绍:

1.生产者生产消息到RabbitMQ Server 可靠性保证

2.RabbitMQ Server中存储的消息如何保证

3.RabbitMQ Server到消费者消息如何不丢

1. 生产者生产消息到RabbitMQ Server可靠性保证

这个过程,消息可能会丢,比如发生网络丢包、网络故障等造成消息丢失,一般情况下如果不采取措施,生产者无法感知消息是否已经正确无误的发送到exchange中,如果生产者能感知到的话,它可以进行进一步的处理动作,比如重新投递相关消息以确保消息的可靠性。

1.1 通常有一种方案可以解决:就是 AMQP协议提供的一个事务机制

RabbitMQ客户端中Channel 接口提供了几个事务机制相关的方法:

channel.txSelect

channel.txCommit

channel.txRollback

源码截图如下:com.rabbitmq.client 包中public interface Channel extendsShutdownNotifier {}接口

在生产者发送消息之前,通过channel.txSelect开启一个事务,接着发送消息, 如果消息投递server失败,进行事务回滚channel.txRollback,然后重新发送, 如果server收到消息,就提交事务channel.txCommit

但是,很少有人这么干,因为这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低。

1.2 幸运的是RabbitMQ提供了一个改进方案,即发送方确认机制(publisher confirm)

首先生产者通过调用channel.confirmSelect方法将信道设置为confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一deliveryTag和multiple参数),这就使得生产者知晓消息已经正确到达了目的地了。

其实Confirm模式有三种方式实现:

  1. 串行confirm模式:producer每发送一条消息后,调用waitForConfirms()方法,等待broker端confirm,如果服务器端返回false或者在超时时间内未返回,客户端进行消息重传。
  2. 批量confirm模式:producer每发送一批消息后,调用waitForConfirms()方法,等待broker端confirm。
  3. 异步confirm模式:提供一个回调方法,broker confirm了一条或者多条消息后producer端会回调这个方法。 我们分别来看看这三种confirm模式

串行confirm

for(int i = 0;i<50;i++){
channel.basicPublish(
exchange, routingKey,
mandatory, immediate,
messageProperties,
message.getContent()
);
if (channel.waitForConfirms()) {
System.out.println("发送成功");
} else {
//发送失败这里可进行消息重新投递的逻辑
System.out.println("发送失败");
}
}

  

批量confirm模式

for(int i = 0;i<50;i++){
channel.basicPublish(
exchange, routingKey,
mandatory, immediate,
messageProperties,
message.getContent()
);
}
if (channel.waitForConfirms()) {
System.out.println("发送成功");
} else {
System.out.println("发送失败");
}

  

上面代码是简单版本的,生产环境绝对不是循环发送的,而是根据业务情况, 各个客户端程序需要定期(每x秒)或定量(每x条)或者两者结合来publish消息,然后等待服务器端confirm。相比普通confirm模式,批量可以极大提升confirm效率。

但是有没有发现什么问题?

问题1: 批量发送的逻辑复杂化了。

问题2: 一旦出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm性能应该是不升反降的。

异步confirm模式

Channel channel = channelManager.getPublisherChannel(namespaceName);
ProxiedConfirmListener confirmListener = new ProxiedConfirmListener();//监听类
confirmListener.setChannelManager(channelManager);
confirmListener.setChannel(channel);
confirmListener.setNamespace(namespaceName);
confirmListener.addSuccessCallbacks(successCallbacks);
channel.addConfirmListener(confirmListener);
channel.confirmSelect();//开启confirm模式
AMQP.BasicProperties messageProperties = null;
if (message.getProperty() instanceof AMQP.BasicProperties) {
messageProperties = (AMQP.BasicProperties) message.getProperty();
}
confirmListener.toConfirm(channel.getNextPublishSeqNo(), rawMsg);
for(int i = 0;i<50;i++){
channel.basicPublish(
exchange, routingKey,
mandatory, immediate,
messageProperties,
message.getContent()
);
}

  

异步模式需要自己多写一部分复杂的代码实现,异步监听类,监听server端的通知消息,异步的好处性能会大幅度提升,发送完毕之后,可以继续发送其他消息。 MQServer通知生产端ConfirmListener监听类:用户可以继承接口实现自己的实现类,处理消息确认机制,此处继承类代码省略,就是上面 ProxiedConfirmListener 类: 下面贴下要实现的接口:

package com.rabbitmq.client;

import java.io.IOException;

/**
* Implement this interface in order to be notified of Confirm events.
* Acks represent messages handled successfully; Nacks represent
* messages lost by the broker. Note, the lost messages could still
* have been delivered to consumers, but the broker cannot guarantee
* this.
*/
public interface ConfirmListener {
/**
** handleAck RabbitMQ消息接收成功的方法,成功后业务可以做的事情
** 发送端投递消息前,需要把消息先存起来,比如用KV存储,接收到ack后删除
**/
void handleAck(long deliveryTag, boolean multiple)
throws IOException; //handleNack RabbitMQ消息接收失败的通知方法,用户可以在这里重新投递消息
void handleNack(long deliveryTag, boolean multiple)
throws IOException;
}

  

上面的接口很有意思,如果是你的话,怎么实现? 消息投递前如何存储消息,ack 和 nack 如何处理消息?

下面看下异步confirm的消息投递流程:

解释下这张图片:

channel1 连续发类1,2,3条消息到RabbitMQ-Server,RabbitMQ-Server通知返回一条通知,里面包含回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外还有一个参数multiple=true,表示到这个序号之前的所有消息都已经得到了处理。这样客户端和服务端通知的次数就减少类,提升类性能。

channel3 发送的消息失败了,生产端需要对投递消息重新投递,需要额外处理代码。 那么生产端需要做什么事情呢?因为是异步的,生产端需要存储消息然后根据server通知的消息,确认如何处理,于是我们面临的问题是:

第一:发送消息之前把消息存起来

第二:监听ack 和 nack 并做响应处理

那么怎么存储呢?

我们分析下,可以使用SortedMap 存储,保证有序,但是有个问题高并发情况下, 每秒可能几千甚至上万的消息投递出去,消息的ack要等几百毫秒的话,放内存可能有内存溢出的风险。所以建议采用KV存储,KV存储承载高并发能力高,性能好,但是要保证KV 高可用,单个有个缺点就是又引入了第三方中间件,复杂度升高。

解决了上面的问题,下面还会遇到一个问题,消息丢失的另一个情况?

事务机制和publisher confirm机制确保的是消息能够正确的发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确的发往至RabbitMQ的交换器,如果此交换器没有匹配的队列的话,那么消息也将会丢失,怎么办?

这里有两个解决方案,

1. 使用mandatory 设置true

2. 利用备份交换机(alternate-exchange):实现没有路由到队列的消息

我们看下RabbitMQ客户端代码方法

Channel 类中 发布消息方法

 void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
throws IOException;

  

解释下:basicPublish 方法中的,mandatory和immediate

/**
* 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue, 那么会调用basic.return方法将消息返回给生产者<br>
* 当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。
*/
@Setter(AccessLevel.PACKAGE)
private boolean mandatory = false; /**
* 当immediate标志位设置为true时,如果exchange在将消息路由到queue(s)时发现对于的queue上没有消费者, 那么这条消息不会放入队列中。
当immediate标志位设置为false时,exchange路由的队列没有消费者时,该消息会通过basic.return方法返还给生产者。
* RabbitMQ 3.0版本开始去掉了对于immediate参数的支持,对此RabbitMQ官方解释是:这个关键字违背了生产者和消费者之间解耦的特性,因为生产者不关心消息是否被消费者消费掉
*/
@Setter(AccessLevel.PACKAGE)
private boolean immediate;

  

所以为了保证消息的可靠性,需要设置发送消息代码逻辑。如果不单独形式设置mandatory=false

使用mandatory 设置true的时候有个关键点要调整,生产者如何获取到没有被正确路由到合适队列的消息呢?通过调用channel.addReturnListener来添加ReturnListener监听器实现,只要发送的消息,没有路由到具体的队列,ReturnListener就会收到监听消息。

channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP
.BasicProperties basicProperties, byte[] body) throws IOException {
String message = new String(body);
//进入该方法表示,没路由到具体的队列
//监听到消息,可以重新投递或者其它方案来提高消息的可靠性。
System.out.println("Basic.Return返回的结果是:" + message);
}
});

  

此时有人问了,不想复杂化生产者的编程逻辑,又不想消息丢失,那么怎么办? 还好RabbitMQ提供了一个叫做alternate-exchange东西,翻译下就是备份交换器,这个干什么用呢?很简单,它可以将未被路由的消息存储在另一个exchange队列中,再在需要的时候去处理这些消息。

那如何实现呢?

简单一点可以通过webui管理后台设置,当你新建一个exchange业务的时候,可以给它设置Arguments,这个参数就是 alternate-exchange,其实alternate-exchange就是一个普通的exchange,类型最好是fanout 方便管理

当你发送消息到你自己的exchange时候,对应key没有路由到queue,就会自动转移到alternate-exchange对应的queue,起码消息不会丢失。

下面一张图看下投递过程:

那么有人有个疑问,上面介绍了,两种方式处理,发送的消息无法路由到队列的方案, 如果备份交换器和mandatory参数一起使用,会有什么效果?

答案是:mandatory参数无效

由于篇幅太长,我会再分一篇文章出来讲下面的内容

2. RabbitMQ Server中存储的消息如何保证消息可靠性和高可用

待续...

3. RabbitMQ Server到消费者消息如何不丢

待续...

再聊聊大厂都是如何使用在生产环境的

待续...

END

原文链接:https://juejin.im/post/5d468591f265da03b810427e

如有收获,请帮忙转发,后续会有更好文章贡献,您的鼓励是作者最大的动力!

欢迎关注我的公众号:架构师的修炼,获得独家整理的学习资源和日常干货推送。

解决RabbitMQ消息丢失问题和保证消息可靠性(一)的更多相关文章

  1. RabbitMQ消息丢失问题和保证消息可靠性-消费端不丢消息和HA(二)

    继续上篇文章解决RabbitMQ消息丢失问题和保证消息可靠性(一) 未完成部分,我们聊聊MQ Server端的高可用和消费端如何保证消息不丢的问题? 回归上篇的内容,我们知道消息从生产端到服务端,为了 ...

  2. RocketMQ消息丢失解决方案:事务消息

    前言 上篇文章,王子通过一个小案例和小伙伴们一起分析了一下消息是如何丢失的,但没有提出具体的解决方案. 我们已经知道发生消息丢失的原因大体上分为三个部分: 1.生产者发送消息到MQ这一过程导致消息丢失 ...

  3. 用SendNotifyMessage代替PostMessage避免消息丢失(WIN7下消息队列的默认长度是10000,队列满后消息将被丢弃)

    大家都知道PostMessage会丢消息,但是消息队列的大小是多少呢,下面做了一个测试. 代码:   1 unit Unit1; 2 3 interface 4 5 uses 6 Windows, M ...

  4. RabbitMQ:消息丢失 | 消息重复 | 消息积压的原因+解决方案+网上学不到的使用心得

    前言 首先说一点,企业中最常用的实际上既不是RocketMQ,也不是Kafka,而是RabbitMQ. RocketMQ很强大,但主要是阿里推广自己的云产品而开源出来的一款消息队列,其实中小企业用Ro ...

  5. RabbitMQ保证消息的顺序性

    当我们的系统中引入了MQ之后,不得不考虑的一个问题是如何保证消息的顺序性,这是一个至关重要的事情,如果顺序错乱了,就会导致数据的不一致.       比如:业务场景是这样的:我们需要根据mysql的b ...

  6. RocketMQ消息丢失解决方案:同步刷盘+手动提交

    前言 之前我们一起了解了使用RocketMQ事务消息解决生产者发送消息时消息丢失的问题,但使用了事务消息后消息就一定不会丢失了吗,肯定是不能保证的. 因为虽然我们解决了生产者发送消息时候的消息丢失问题 ...

  7. MQ在高并发环境下,如果队列满了,如何防止消息丢失?

    1.为什么MQ能解决高并发环境下的消息堆积问题? MQ消息如果堆积,消费者不会立马消费所有的消息,不具有实时性,所以可以解决高并发的问题. 性能比较好的消息中间件:Kafka.RabbitMQ,Roc ...

  8. im消息丢失插件

    https://github.com/laughin/mocamsg mocamsg Moca message interceptor Openfire网络不好的情况下经常丢消息,一般情况都是服务器端 ...

  9. 消费端如何保证消息队列MQ的有序消费

    消息无序产生的原因 消息队列,既然是队列就能保证消息在进入队列,以及出队列的时候保证消息的有序性,显然这是在消息的生产端(Producer),但是往往在生产环境中有多个消息的消费端(Consumer) ...

随机推荐

  1. nestjs中typeorm进行事物操作

    https://typeorm.io/#/transactions 两种方法

  2. python下载后出现python 已停止工作

    背景: 在执行IDLE或者在terminal窗口执行 python命令时出现如下提示,修改了防火墙关闭也不行,找不到解决办法? 如图: [解决方案] 1.卸载重装python,确保python版本与系 ...

  3. .Net类库 压缩文件 与 Ionic.Zip 批量压缩不同目录文件与解压 文件

    using System; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; us ...

  4. kvm 学习(二)镜像

    Linux下 如何通过命令行使用现有的镜像创建.启动kvm虚拟机 这里假定已经创建好了相应的镜像: eg:我这里制作的镜像名称为zu1-centos7.img # ls zu1-centos7.img ...

  5. "error":"bucket is protected"

    这个是我做图片上传的时候,后台返回了图片,把图片链接放到浏览器 其实是因为后台配置了,图片必须以 结尾.就是压缩的图,所以其他格式结尾的是不会显示的哈

  6. Bean Shell常用内置变量

    JMeter在它的BeanShell中内置了变量,用户可以通过这些变量与JMeter进行交互,其中主要的变量及其使用方法如下: log:写入信息到jmeber.log文件,使用方法:log.info( ...

  7. Vue源码分析(二) : Vue实例挂载

    Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...

  8. 分区工具parted的使用方法

    一.         parted的用途及说明 概括使用说明: parted用于对磁盘(或RAID磁盘)进行分区及管理,与fdisk分区工具相比,支持2TB以上的磁盘分区,并且允许调整分区的大小.   ...

  9. 关于路由跟踪指令---traceroute

    我们都用过ping命令来检查主机与目标地址是否连通,自己的主机与目标地址的通讯包通讯速率,所谓的通讯包也就是那些什么TCP/IP,UDP包. 原理:https://www.cnblogs.com/be ...

  10. sklearn里计算roc_auc_score,报错ValueError: bad input shape

    用sklearn的DecisionTreeClassifer训练模型,然后用roc_auc_score计算模型的auc.代码如下 clf = DecisionTreeClassifier(criter ...