RabbitMQ 在项目中的应用及相关问题
一、项目中 RabbitMQ 的使用场景
在我们的项目中,很多地方都使用了 RabbitMQ,它是服务通信的主要方式之一。项目中服务通信主要有两种方式实现:
- 通过 Feign 实现服务调用。
- 通过 MQ 实现服务通信。基本上除了查询请求之外,大部分的服务调用都采用 MQ 实现的异步调用。
二、选择 RabbitMQ 的原因及好处
(一)选择原因
RabbitMQ 功能丰富,支持各种消息收发模式(简单队列模式、工作队列模式、路由模式、直接模式、主题模式等),支持延迟队列、惰性队列,天然支持集群,保证服务的高可用,同时性能非常不错,社区也比较活跃,文档资料非常丰富。
(二)好处
- **吞吐量提升:**无需等待订阅者处理完成,响应更快速。
- **故障隔离:**服务没有直接调用,不存在级联失败问题。
- 调用间没有阻塞,不会造成无效的资源占用。
- 耦合度极低,每个服务都可以灵活插拔,可替换。
- **流量削峰:**不管发布事件的流量波动多大,都由 Broker 接收,订阅者可以按照自己的速度去处理事件。
(三)缺点
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
三、如何保证消息不丢失
消息从发送到消费者接收会经历多个过程,每一步都可能导致消息丢失。RabbitMQ 针对这些问题给出了解决方案:
(一)消息发送到交换机丢失
发布者确认机制(publisher-confirm),消息发送到交换机失败会向生产者返回 ACK,生产者通过回调接收发送结果,如果发送失败,重新发送,或者记录日志人工介入。
(二)消息从交换机路由到队列丢失
发布者回执机制(publisher-return),消息从交换机路由到队列失败会向生产者返回失败原因,生产者通过回调接收回调结果,如果发送失败,重新发送,或者记录日志人工介入。
(三)消息保存到队列中丢失
MQ 持久化(交换机持久化、队列持久化、消息持久化)。
(四)消费者消费消息丢失
消费者确认机制、消费者失败重试机制。
通过 RabbitMQ 本身所提供的机制基本上可以保证消息不丢失,但因为一些特殊原因还是会发生消息丢失问题,例如回调丢失、系统宕机、磁盘损坏等,这种概率很小。如果想规避这些问题,进一步提高消息发送的成功率:
可以设计一个消息状态表,主要包含消息 id、消息内容、交换机、消息路由 key、发送时间、签收状态等字段。发送方业务执行完毕后,向消息状态表保存一条消息记录,消息状态为未签收,之后再向 MQ 发送消息。消费方接收消息消费完毕后,向发送方发送一条签收消息,发送方接收到签收消息之后,修改消息状态表中的消息状态为已签收。之后通过定时任务扫描消息状态表中这些未签收的消息,重新发送消息,直到成功为止。对于已经完成消费的消息定时清理即可。
四、消息重复消费问题的解决方案
在使用 RabbitMQ 进行消息收发时,如果发送失败或者消费失败会自动进行重试,可能会导致消息的重复消费。解决方案如下:
(一)每条消息设置一个唯一的标识 id。
(二)幂等方案。
- token + redis。
- 分布式锁。
- 数据库锁(悲观锁、乐观锁)。
五、解决 100 万消息堆积的思路
解决消息堆积有三种思路:
(一)提高消费者的消费能力
- 使用多线程消费。
- 增加更多消费者,提高消费速度。
- 使用工作队列模式,设置多个消费者消费同一个队列中的消息。
(二)扩大队列容积,提高堆积上限
- 使用 RabbitMQ 惰性队列。
- 惰性队列:接收到消息后直接存入磁盘而非内存,消费者要消费消息时才会从磁盘中读取并加载到内存,支持数百万条的消息存储。
六、保证消费顺序性的方法
一个队列只设置一个消费者消费即可,多个消费者之间无法保证消息消费顺序性。
七、RabbitMQ 延迟队列的实现方案
RabbitMQ 的延迟队列有两种实现方案:
(一)使用消息过期 TTL + 死信交换机。
(二)使用延迟交换机插件。
八、RabbitMQ 设置消息过期的方式
RabbitMQ 设置消息过期的方式有两种:
(一)为队列设置过期时间
所有进到这个队列的消息就会具有统一的过期时间。
(二)为消息单独设置过期时间
注意:队列过期和消息过期同时存在时,会以时间短的时间为准。
示例代码:
@Bean
public Queue ttlQueue(){
return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
.ttl(10000) // 设置队列的超时时间,10 秒
.deadLetterExchange("dl.ttl.direct") // 指定死信交换机
.build();
}
@Test
public void testTTLQueue() {
// 创建消息
String message = "hello, ttl queue";
// 消息 ID,需要封装到 CorrelationData 中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
// 记录日志
log.debug("发送消息成功");
}
RabbitMQ 队列消息过期的机制是判断队列头部元素是否过期,如果队里头部消息没有到过期时间,中间消息到了过期时间,这个消息也不会被自动剔除。
九、消息成为死信的情况
当一个队列中的消息满足下列情况之一时,就会成为死信(dead letter):
(一)消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false。
(二)消息是一个过期消息,超时无人消费。
(三)要投递的队列消息满了,无法投递。
十、死信交换机及队列绑定方法
死信交换机和正常的交换机没有什么不同,如果一个包含死信的队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机。
为队列绑定死信交换机,只需要设置队列属性 dead-letter-exchange 即可。
十一、RabbitMQ 的高可用机制
RabbitMQ 是基于 Erlang 语言编写,天然支持集群模式。RabbitMQ 的集群有两种模式:
(一)普通集群
是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。会在集群的各个节点间共享部分数据,包括交换机、队列元信息,但不包含队列中的消息。当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回。队列所在节点宕机,队列中的消息就会丢失。
(二)镜像集群
是一种主从集群,在普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。
- 交换机、队列、队列中的消息会在各个 mq 的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点。
- 所有操作都是主节点完成,然后同步给镜像节点。
- 主宕机后,镜像节点会替代成新的主节点。
EMQ 在项目中的应用及相关问题
一、EMQ 简介及项目中的应用场景
- EMQ 是什么:EMQ X 是开源社区中最流行的 MQTT 消息服务器,广泛应用于物联网、移动互联网、智能硬件、车联网、电力能源等领域,如物联网 M2M 通信、Android 消息推送、移动即时消息、智能硬件、车联网通信等。
- 项目中的应用:在我们的项目中,主要使用 EMQ 实现服务器和物联网设备之间的信息传输,以及作为消息队列产品实现各个微服务之间的数据传输,例如设置状态实时监控、自动维修工单创建和自动补货工单创建、订单创建以及支付结果确认、设备出货控制、设置出货结果通知处理等。
二、使用 EMQ 保证消息不丢失的方法
在 MQTT 协议中规定了消息服务质量(Quality of Service),保证不同网络环境下消息传递的可靠性。MQTT 消息服务质量 QoS 等级有三个级别:
- 0:消息最多传递一次,可能会存在消息丢失。
- 1:消息至少传递一次,不会出现消息丢失,但可能会出现消息重复。
- 2:消息仅传递一次,不会出现消息丢失,也不会出现消息重复。
三、使用 EMQ 避免消息重复消费的方法
同样利用 MQTT 协议中的消息服务质量(Quality of Service)来避免消息重复消费。MQTT 消息服务质量 QoS 等级为 2 时,消息仅传递一次,不会出现消息丢失和重复消费的情况。
四、EMQ 支持延迟消息及实现方法
EMQ X 的延迟发布功能可以实现按照用户配置的时间间隔延迟发布 PUBLISH 报文的功能。当客户端使用特殊主题前缀 $delayed/{DelayInteval}
发布消息到 EMQ X 时,将触发延迟发布功能。延迟发布的功能是针对消息发布者而言的,订阅方只需要按照正常的主题订阅即可。例如:
- 前缀:
random
、sticky
、hash
。 - 真实主题名:
$queue/
、t/1
。 - 描述:不带群组共享订阅时为
$queue/t/1
、t/1
;带群组共享订阅时为$share/abc
,均衡策略可以是round_robin
等,如$share/组名称/t/1
。在所有订阅者中随机选择、按照订阅顺序轮询、按照发布者 ClientID 的哈希值、一直发往上次选取的订阅者等不同策略。
五、使用 EMQ 实现点对点消息和发布订阅消息的方法
默认情况下,EMQ 中的消息会发送给所有订阅了主题的订阅者,属于发布订阅机制。EMQ X 支持两种格式的共享订阅前缀:
模式 | 示例 | 前缀 | 真实主题名 |
---|---|---|---|
不带群组共享订阅 | Squeue/t/1 | Squeue/ | t/1 |
带群组共享订阅 | $share/组名称/t/1 | $share/abc | t/1 |
在 EMQ 中,如果想实现点对点消息,可以采用不带群组的共享订阅。通过这种方式,消息只会被订阅者列表中的某一个订阅者接收。
并且,可以在配置文件中配置负载均衡的策略,例如:
broker.shared_subscription_strategy = random
均衡策略 | 描述 |
---|---|
random | 在所有订阅者中随机选择 |
round_robin | 按照订阅顺序轮询 |
sticky | 一直发往上次选取的订阅者 |
hash | 按照发布者 ClientID 的哈希值 |
如果想不通的群组都只能有一个订阅者接收到消息, 可以使用带群组的共享订阅, 这样每个群组中都会有一个订阅者接收到消息