05.如何设计可靠的消息队列系统?
面试考察内容
在面试过程中遇到这个问题,面试官主要考察一下几个方面:
- 消息队列基础知识:候选人对消息队列基本概念的理解,包括什么是消息队列、它解决了什么问题等。
- 系统架构能力:候选人如何构思整体架构,包括消息的发送、存储、消费机制以及如何保证消息的可靠传递。
- 高可用性和可扩展性:系统如何处理大量并发请求,如何在节点故障时保持服务正常运行。、
- 性能优化:对于延迟敏感的应用场景,如何优化消息队列以减少延迟。
- 容错机制:面对网络中断或服务器宕机等情况,系统如何确保数据不丢失。
- 安全性:如何保护消息传输的安全性,防止未授权访问。
- 监控与维护:如何设置有效的监控体系,及时发现并解决问题
问题回答思路
什么是消息队列
消息队列(Message Queue,简称MQ)指保存消息的一个容器,其实本质就是一个保存数据的队列。
其中消息队列是消息中间,指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的构建。
消息中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削峰, 消息通讯等问题,实现高性能,高可用,可伸缩和最终一致性的系统架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。
消息队列的应用场景
1、 异步处理
异步处理就是把一些非及时反馈的核心业务,通过异步的方式并行处理,从而减少响应时间,提高系统的吞吐量。
比如电商的订单处理系统,当用户下单后,系统需要执行一系列操作,包括库存检查、支付处理、订单确认、发货通知等。这些操作如果同步执行,可能会导致响应时间过长,用户体验不佳。采用异步方式及时告诉用户反馈提交订单信息,后续步骤发送到消息队列并发处理。

以上面下单为例,用户下单后需要有库存检查、支付处理、订单确认、发货通知一系列业务处理,假设每个业务需要处理100ms,那么串行的方式就需要400ms的时间,但是就用户反馈而言超过200ms用户就会有明显使用卡顿感受。使用并行的方式可能也就需要200ms的时间,不仅可以提升用户的使用体验,还能提高系统的吞吐量。
2、 应用解耦
想象一下,在一个典型的电商系统中,当用户完成下单后,订单系统需要通知积分系统进行积分处理。传统的做法是订单系统直接调用积分系统的接口。这种直接调用的方式会让两个系统紧密耦合在一起。如果积分系统出现故障或无法访问,那么积分处理就会失败,进而导致整个订单流程受阻。
使用消息队列的解决方案:
**订单系统:**生成订单后,将相关信息发送到消息队列。 **消息队列:**存储并传递消息。 **积分系统:**订阅消息队列中的相关主题,接收并处理订单信息。
通过这种方式,订单系统不再直接依赖积分系统,而是通过消息队列进行通信。即使积分系统暂时不可用,订单系统仍然可以正常运行,消息队列会暂存这些消息,待积分系统恢复后继续处理。

引入消息队列后,当用户下单时,订单系统完成下单操作后,会将消息写入消息队列,并立即返回用户下单成功的确认。积分系统通过订阅消息队列中的下单消息来获取通知,从而进行积分处理。这种方式实现了订单系统与积分系统的解耦。
如果在下单过程中积分系统出现异常,也不会影响用户的正常下单流程。因为订单系统在写入消息队列后,就不再关心后续的积分操作,确保了订单处理的独立性和可靠性。
3、 流量削峰
一个电商平台计划在即将到来的“双11”大促期间举行一场大规模的秒杀活动。预计在活动开始时会有大量的用户同时涌入系统,尝试抢购限量商品。这种突发的高流量可能会导致系统过载,影响用户体验甚至导致系统崩溃。
为了防止大量请求直接去请求服务,这个时候一般需要在加入消息队列,用户在前端的海量请求全部加入到消息队列当中而不是直接处理订单,后台秒杀订单处理业务根据消息队列的请求信息做后续的处理。
具体步骤如下:

4、 消息通讯
作为消息队列的最基础的作用就是让服务应用之间进行数据通信,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等点对点通讯。在微服务系统中,消息队列一般采用定于定于模式。
点对点:Queue,不可重复消费
消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。

发布/订阅:Topic,可以重复消费
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。

参考的技术选型
其实说是系统设计,更多的是考虑你在特定场景下如何选择合适的消息队列,并对所选取的消息队列的核心内容能够了解。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ等,我们就来对比一下这几个消息队列的情况。
特性 | Kafka | RocketMQ | RabbitMQ | ActiveMQ |
---|---|---|---|---|
单机吞吐量 | 10万级 | 10万级 | 万级 | 10万级 |
开发语言 | Scala | Java | Erlang | Java |
高可用 | 分布式 | 分布式 | 主从 | 分布式 |
消息延迟 | ms级 | ms级 | us级 | ms级 |
消息丢失 | 理论上不会丢失 | 理论上不会丢失 | 低 | 低 |
消费模式 | 拉取 | 推拉 | 推拉 | |
持久化 | 文件 | 内存,文件 | 内存,文件,数据库 | |
支持协议 | 自定义协议 | 自定义协议 | AMQP,XMPP, SMTP,STOMP | AMQP,MQTT,OpenWire,STOMP |
社区活跃度 | 高 | 中 | 高 | 高 |
管理界面 | web console | 好 | 一般 | |
部署难度 | 中 | 低 | ||
部署方式 | 独立 | 独立 | 独立 | 独立,嵌入 |
成熟度 | 成熟 | 比较成熟 | 成熟 | 成熟 |
综合评价 | 优点:拥有强大的性能及吞吐量,兼容性很好。 缺点:由于支持消息堆积,导致延迟比较高。 | 优点:性能好,稳定可靠,有活跃的中文社区,特点响应快。 缺点:兼容性较差,但随着影响力的扩大,该问题会有改善。 | 优点:产品成熟,容易部署和使用,拥有灵活的路由配置。 缺点:性能和吞吐量较差,不易进行二次开发。 | 优点:产品成熟,支持协议多,支持多种语言的客户端。 缺点:社区不活跃,存在消息丢失的可能。 |
容错机制和恢复
如何保证你的消息队列减少故障、可用性强,简单来说这一系列的问题就是冗余设计,无非就是备份、恢复、多节点切换这三板斧。
备份:
- 消息持久化:确保消息在发送到消息队列后被持久化存储,即使消息队列服务重启或故障,消息也不会丢失。
- 日志记录:记录所有操作的日志,包括消息的发送、接收和处理,以便在故障发生时进行回溯和恢复。
恢复与切换
- 多副本:使用多个消息队列实例,并在不同的节点上复制消息,以防止单点故障。
- 主从复制:设置主从架构,主节点负责写入,从节点负责读取。如果主节点故障,从节点可以接管。
- 自动切换:当检测到某个节点故障时,自动切换到备用节点,确保服务的连续性。
- 心跳检测:定期发送心跳信号,检查消息队列节点的状态。如果某个节点没有响应,立即启动故障切换机制
设计方案
核心考点:
消息存储方式:消息队列需要将消息存储在某种媒介中,一般采用内存或者磁盘存储。在内存存储的情况下,可以快速的读写消息,但是可能会丢失消息,因为内存中的消息没有持久化。而采用磁盘存储,可以持久化消息,但是读写速度相对慢一些。
消息传递协议:消息队列需要定义消息传递的协议,包括消息格式、消息队列的地址等信息**。我们可以使用成熟的RPC框架(如Dubbo或Thrift)实现生产者和消费者与Broker之间的通信。**
消息的持久化和确认机制:在消息队列中,需要实现消息的持久化和确认机制,确保消息不会丢失或重复消费。一般的做法是将消息存储在磁盘中,并在消费者确认消费完成后再删除消息。
消息的分发方式:消息队列需要实现消息的分发方式,包括点对点和广播两种方式。在点对点方式下,每个消费者只会接收到自己订阅的消息;在广播方式下,每个消费者都会接收到所有的消息。
根据场景架构选型
前面提到了消息队列的两种选型,分别是点对点和发布-订阅两种结构,接下来在设计消息队列的时候需要考虑以下情况,应该选择哪一种架构,如果是消息需要被多服务消费的话就采用发布-订阅的结构,如果是前期服务比较小的话可以采用点对点通信。
一般来说,大部分的考点都是选择发布-订阅这种结构进行问答的。
消息队列如何传递消息
在消息队列中,有多种消息的传递方式,如轮询、长连接,还有长轮询。一般都是支持推拉结合的方式。或者基于拉实现推的机制。
接下来分别介绍不同消息队列是如何传递消息。这里主要考虑RocketMQ的消息传递方式。
RabbitMQ的消息分发方式分5种模式,分别是简单模式、工作队列模式、发布订阅模式、路由模式、主题模式以及RPC模式。
RocketMQ 支持两种消息模式:广播消费( Broadcasting )和集群消费( Clustering )。
广播消费:当使用广播消费模式时,RocketMQ 会将每条消息推送给集群内所有的消费者,保证消息至少被每个消费者消费一次。
广播模式下,RocketMQ 保证消息至少被客户端消费一次,但是并不会重投消费失败的消息,因此业务方需要关注消费失败的情况。并且,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过。
集群消费:当使用集群消费模式时,RocketMQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可。
集群模式下,每一条消息都只会被分发到一台机器上处理。但是不保证每一次失败重投的消息路由到同一台机器上。一般来说,用集群消费的更多一些。
消息的可靠性保证
消息队列的容错性和可用性:消息队列需要实现高可用和容错机制,以确保消息的可靠传输。一般的做法是采用主从复制、集群模式或者分布式架构来实现。
1. 持久化
- Kafka通过将所有数据写入磁盘并复制到多个broker上来保证持久性。每个topic可以被分成多个partition,每个partition在不同的broker上存储副本。
- Kafka使用顺序追加的方式写入日志文件,这种方式非常高效且有利于快速恢复。
- 生产者可以选择不同的acknowledgment模式(acks)来控制消息的可靠性:
acks=0
:生产者不等待任何确认就认为发送成功。acks=1
:leader分区收到消息后立即返回确认。acks=all
:只有当所有同步副本都确认收到消息后才返回确认。
2. 确认机制
- 生产者发送消息时可以通过设置
acks
参数来指定需要多少个副本确认接收后才算发送成功。 - 消费者处理完消息后会向Kafka发送一个偏移量(offset)更新请求,标记为已消费的消息。如果消费者在处理过程中失败了,它可以从上次提交的偏移量开始重新消费。
- Kafka提供了两种主要的消费者API:旧版的高阶API和新版的低阶API。在新版本中,推荐使用
commitSync()
或commitAsync()
方法来提交偏移量,确保消息至少被处理一次。
3. 消息去重
- Kafka本身并不直接支持消息去重功能。但是,可以通过业务逻辑结合外部数据库或其他服务来实现这一点。
- 一种常见的做法是利用消息中的唯一标识符(如UUID),在处理消息之前检查该标识是否已经存在于数据库中。如果存在,则跳过这条消息;否则,进行处理并将标识存入数据库。
4. 高可用性和容错性
- Kafka通过维护多份数据副本来提高系统的可用性和容错能力。每个topic的每个partition都有一个leader和若干followers。当leader不可用时,其中一个follower会被选举为新的leader。
- 用户可以通过配置
replication.factor
来指定每个partition的副本数量,从而增加数据冗余度。 - Kafka集群支持自动故障转移,当某个节点发生故障时,其他节点能够继续提供服务。
5. 流量控制
- Kafka内置了流量控制机制来防止生产者的发送速率超过消费者的处理能力。
- 当broker检测到其内部缓冲区接近满载时,它可以减慢甚至暂停接受来自生产者的新消息,直到有足够的空间为止。
- 此外,还可以通过调整生产者端的
max.in.flight.requests.per.connection
等参数来控制未确认请求的数量,进一步优化性能与可靠性之间的平衡。