进程之间的通信在开发过程中十分常见,那么如何保证进程之间消息通信的可靠性,下面分别从分布式系统(消息中间件RabbitMQ)和单机系统(ZeroMQ)来说明他们在消息传输中,是如何保证消息的不丢失的。
1、RabbitMQ的可靠性传输
1.1 RabbitMQ出现消息丢失的情况及其解决办法
如图所示,RabbitMQ丢失消息的情况可以发送在任何一个节点。
1.1.1 生产者没有成功把消息发送到MQ
a、丢失的原因:因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有成功接收到消息,但是生产者却以为MQ成功接收到了消息,不会再次重复发送该消息,从而导致消息的丢失。
b、解决办法: 有两个解决办法:事务机制和confirm机制,最常用的是confirm机制。
事务机制:
RabbitMQ 提供了事务功能,生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。伪代码如下:
confirm机制:
RabbitMQ可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,生产者每次写的消息都会分配一个唯一的 id,如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。
注意:RabbitMQ的事务机制是同步的,很耗型能,会降低RabbitMQ的吞吐量。confirm机制是异步的,生成者发送完一个消息之后,不需要等待RabbitMQ的回调,就可以发送下一个消息,当RabbitMQ成功接收到消息之后会自动异步的回调生产者的一个接口返回成功与否的消息。
1.1.2 RabbitMQ接收到消息之后丢失了消息
a、丢失的原因:RabbitMQ接收到生产者发送过来的消息,是存在内存中的,如果没有被消费完,此时RabbitMQ宕机了,那么再次启动的时候,原来内存中的那些消息都丢失了。
b、解决办法:开启RabbitMQ的持久化。当生产者把消息成功写入RabbitMQ之后,RabbitMQ就把消息持久化到磁盘。结合上面的说到的confirm机制,只有当消息成功持久化磁盘之后,才会回调生产者的接口返回ack消息,否则都算失败,生产者会重新发送。存入磁盘的消息不会丢失,就算RabbitMQ挂掉了,重启之后,他会读取磁盘中的消息,不会导致消息的丢失。
c、持久化的配置:
第一点是创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
注意:持久化要起作用必须同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
1.1.3 消费者弄丢了消息
a、丢失的原因:如果RabbitMQ成功的把消息发送给了消费者,那么RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但如果就在此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。
b、解决的办法:简单来说,就是必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次在自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
1.2 如何防止重复消费
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决思路是:
保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;
在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
这个问题针对业务场景来答分以下几点:
1. 如果消息是做数据库的insert操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
2. 如果消息是做redis的set的操作,不用解决,因为无论set几次结果都是一样的,set操作本来就算幂等操作。
3. 如果以上两种情况还不行,可以准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将
1.3 RabbitMQ的优缺点
a、优点:支持集群部署,支持消息的持久化,使用erlang语言开发,并发性能高,吞吐量大。
b、缺点:重量级消息队列,需要单独部署服务。
2、ZeroMQ的可靠性传输
2.1 ZeroMQ有什么不同
ZeroMQ(简称ZMQ)是一个基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。
ZMQ不是单独的服务,而是一个嵌入式库,它封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。
2.2 ZeroMQ如何保证消息的可靠性
ZMQ保证消息可靠性的原理跟RabbitMQ基本类似。除此之外还有其他的特点:
1. 它在后台线程异步的处理I/O。这些后台线程使用无锁数据结构与程序线程交流,所以并发ZMQ程序不需要锁、信号量、或其它等待状态。
2. 组件可以动态的来来去去,而ZMQ会自动重连。这意味着你可以按任意顺序启动组件。你可以创建“面向服务架构”(SOAs),服务可以随时加入和离开网络。
3. 当需要时它自动将消息排入队列。以智能的方式,消息排入队列前推送消息到尽可能靠近接收者。
4. 它有几种办法处理满溢队列(称为“高水位线”)。当队列填满时,ZMQ自动阻塞发送者,或丢弃消息,取决于你用的消息传递方式(所谓的“模式”)。
5. 它让你的程序用任意传输方式来相互交谈:TCP、多播、进程内、进程间。更改传输方式时无需更改代码。
6. 安全处理低速/阻塞的读者,使用的是取决于消息传递模式的不同策略。
7. 它让你路由消息使用各种模式如请求-应答和发布-订阅。这些模式是你创建拓扑、网络结构的方式。
8. 它让你用一个调用就能创建代理来做队列、转发、或捕获消息。代理可以降低网络的互联复杂度。
9. 它使用简单的线上组帧,转发整个消息并精确重现其发送时的样子。如果你写入一个10K的消息,就能接收一个10K的消息。
10. 它不在消息上强加任何格式。消息就是零到千兆大小的二进制大对象。想要描述数据时你可以在其上选择一些其它产品,例如谷歌的协议缓冲(protocol buffers)、外部数据表示法(XDR)、或其它。
11. 它智能的处理网络错误。有时它会重试,有时它告知你一个操作失败了。
2.3 ZeroMQ的优缺点
a、优点:轻量级,不需要单独部署服务,速度快,作为单机C++和JAVA进程之间可靠性通信的首选。
b、缺点:消息队列不支持持久化,不支持集群,不支持高可用。