6、持久化
持久化可以提高 RabbitMQ 的可靠性,以防在异常情况(重启、关闭、右机等)下的数据丢失。
RabbitMQ的持久化分为三个部分:
-
1.交换器的持久化:交换器的持久化是在声明交换器
exchangeDeclare
时将durable
参数置为true
实现的。如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说, 建议将其置为持久化的。
-
2.队列的持久化:队列的持久化是在声明队列
queueDeclare
时将durable
参数置为true
实现的。如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。
-
3.消息的持久化:通过将消息的投递模式(
BasicProperties
中的deliveryMode
属性)设置为2
即可实现消息的持久化。
注意要点:
可以将所有的消息都设直为持久化,但是这样会严重影响 RabbitMQ 的性能(随机)。写入磁盘的速度比写入内存的速度慢得不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吐吞量之间做一个权衡。
将交换器、队列、消息都设置了持久化之后就能百分之百保证数据不丢失了吗? 答案是否定的。
- 首先从消费者来说,如果在订阅消费队列时将 autoAck 参数设置为 true ,那么 当消费者接收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。这种情况很好解决,将 autoAck 参数设置为 false,并进行手动确认
- 其次,在持久化的消息正确存入 RabbitMQ 之后,还需要有一段时间(虽然很短,但是不可忽视〉才能存入磁盘之中。 如果在这段时间内 RabbitMQ 服务节点发生了岩机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。
- 这个问题怎么解决呢?
这里可以引入 RabbitMQ 的镜像队列机制,相当于配置了副本,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave), 这样有效地保证了高可用性,除非整个集群都挂掉。虽然这样也不能完全保证 RabbitMQ 消息不丢失,但是配置了镜像队列要比没有配置镜像队列的可靠性要高很多,在实际生产环境中的关键业务队列一般都会设置镜像队列。
要保证消息持久化成功的条件有哪些?
- 声明交换器必须设置持久化 durable 设置为 true.
- 声明队列必须设置持久化 durable 设置为 true.
- 消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)。
- 消息已经到达持久化交换器。
- 消息已经到达持久化队列。
以上几个条件都满足才能保证消息持久化成功。
7、生产者确认
如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。
RabbitMQ 针对这个问题,提供了两种解决方式:
- 通过事务机制实现:
- 通过发送方确认(publisher confirm) 机制实现
7.1 事物机制
RabbitMQ 客户端中与事务机制相关的方法有三个:channel. txSelect()
、 channel.txCommit()
和 channel.txRollback()
。
- channel.txSelect() :用于将当前的信道设置成事务模式.
- channel.txCommit() :用于提交事务
- channel.txRollback() :用于事务回滚。
try {
channel.txSelect(); // 开启事务
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, "transaction messages".getBytes());
channel.txCommit(); // 提交
}catch(Exception e){
e.printStackTrace();
channel.txRollback(); // 回滚
}
如果要发送多条消息,则将channel.basicPublish和channel.txCommit等方法包裹进循环内即可,如下所示:
channel.txSelect(); // 开启事务
for (int i=0;i<LOOP_TIMES;i++) {
try {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, "transaction messages".getBytes());
channel.txCommit(); // 提交
}catch(Exception e){
e.printStackTrace();
channel.txRollback(); // 回滚
}
}
事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,但是使用事务机制会“吸干”RabbitMQ的性能。RabbitMQ提供了一个改进方案,即发送方确认机制。
7.2 发送方确认机制
生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是持久化的,那么确认消息会在消息写入磁盘之后发出。
try {
channel.confirmSelect () ; //将信道置为publisher confirm模式
// 之后正常发送消息
channel.basicPublish("exchange","routingKey", null,"publisher confirm test".getBytes ());
if (!channel.waitForConfirms()) { // 等待确认
System.out.println("send message failed"); // 发送失败了
// do something else...
}
}catch (InterruptedException e) {
e.printStackTrace();
}
如果发送多条消息,只需要将channel.basicPublish
和channel.waitForConfirms
方法包裹在循环里面即可,可以参考事务机制,不过不需要把channel.confirmSelect
方法包裹在循环内部。
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用程序便可以通过回调方法来处理该确认消息。如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack命令。
注意要点:
- 事务机制和publisher confirm机制两者是互斥的,不能共存。
- 事务机制和publisher confirm机制确保的是消息能够正确地发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确地发往至RabbitMQ的交换器,如果此交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。更进一步地讲,发送方要配合mandatory参数或者备份交换器一起使用来提高消息传输的可靠性。
publisher confirm的优势在于并不一定需要同步确认。这里我们改进了一下使用方式,总结有如下两种:
-
批量confirm方法:每发送一批消息后,调用channel.waitForConfirms方法,等待服务器的确认返回。好处是相比于之前示例中的普通确认提升了效率,问题是出现返回Basic.Nack或者超时情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm的性能应该是不升反降的。
-
异步confirm方法:提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理。
8、消息顺序性
当RabbitMQ队列拥有多个消费者时,队列收到的消息将以轮询的分发方式发送给消费者。很多时候轮询的分发机制也不是那么优雅,为了达到各个消费者负载均衡,可以使用channel.basicQos
方法。channel.basicQos
方法允许限制信道上的消费者所能保持的最大未确认消息的数量。注意:basicQos
的使用对于拉模式的消费方式无效。
消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。目前很多资料显示RabbitMQ 的消息能够保障顺序性,这是不正确的,或者说这个观点有很大的局限性。
那么哪些情况下 RabbitMQ 的消息顺序性会被打破呢?
- 如果生产者使用了事务机制,在发送消息之后遇到异常进行了事务回滚,那么需要重新补偿发送这条消息,如果补偿发送是在另一个线程实现的 ,那么消息在生产者这个源头就出现了错序。
- 如果启用 publisher confirrn 时,在发生超时、中断,又或者是收到 RabbitMQ 的 Basic.Nack 命令时,那么同样需要补偿发送,结果与事务机制一样会错序。
- 如果生产者发送的消息设置了不同的超时时间,井且也设置了死信队列, 整体上来说相当于一个延迟队列,那么消费者在消费这个延迟队列的时候,消息的顺序必然不 会和生产者发送消息的顺序一致。
- 如果消息设置了优先级,那么消费者消费到的消息也必然不是顺序性的 。
解决方案:在消息体内添加全局有序标识(类似 Sequence ID) 来实现 。
9、消息传输保障
一般消息中间件的消息传输保障分为三个层级。
- At most once:最多一次。消息可能会丢失,但绝不会重复传输。
- At least once:最少一次。消息绝不会丢失,但可能会重复传输。
- Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。
保证最少一次,即保证数据可靠性、不丢失,需要做到以下几个方面:
- 消息生产者需要开启事务机制或者生产者确认机制(publisher confirm机制),以确保消息可以可靠地传输到RabbitMQ中。
- 消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。
- 消息和队列都需要进行持久化处理,以确保RabbitMQ 服务器在遇到异常情况时不会造成消息丢失。或者使用镜像队列机制,相当于配置了副本。
- 消费者在消费消息的同时需要将autoAck设置为false,然后通过手动确认的方式去确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失。
- 消息补偿机制:RabbitMQ有自动消息补偿机制(重试机制),即MQ向消费者推消息,消费者失败抛异常,会自动重试,重试次数自己设定。
10、vhost
每一个RabbitMQ服务器都能创建虚拟的消息服务器,我们称之为虚拟主机(virtual host),简称为vhost。
每一个vhost本质上是一个独立的小型RabbitMQ服务器,拥有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限。vhost就像是虚拟机与物理服务器一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地运行数据,它既能将同一个RabbitMQ中的众多客户区分开,又可以避免队列和交换器等命名冲突。
vhost之间是绝对隔离的,无法将vhost1中的交换器与vhost2中的队列进行绑定,这样既保证了安全性,又可以确保可移植性。如果在使用RabbitMQ达到一定规模的时候,建议用户对业务功能、场景进行归类区分,并为之分配独立的vhost。
vhost是AMQP概念的基础,客户端在连接的时候必须制定一个vhost。RabbitMQ默认创建的vhost为“/”。
11、集群
RabbitMQ只要求在集群中至少有一个磁盘节点,所有其他节点可以是内存节点。如果集群中唯一的磁盘节点崩溃,集群仍然可以保持运行,但是直到将该节点恢复到集群前,你无法更改任何东西。所以在建立集群的时候应该保证有两个或者多个磁盘节点的存在。
除非使用的是RabbitMQ的RPC功能,否则创建队列、交换器及绑定关系的操作确是甚少,大多数的操作就是生产或者消费消息。为了确保集群信息的可靠性,或者在不确定使用磁盘节点或者内存节点的时候,建议全部使用磁盘节点。