1、什么是消息队列(MQ)
MQ 的全称是 Message Queue(消息队列) ,它是一种在应用程序之间进行异步通信的机制,也是现代分布式系统中一个重要的中间件组件。
你可以把它想象成一个智能的"快递柜"或"邮局" :
- 生产者 (Producer) :就像寄快递的人,把包裹(消息 )放进快递柜(队列)。
- 消费者 (Consumer) :就像取快递的人,在方便的时候从快递柜取出包裹进行处理。
通过这种方式,发送方和接收方不需要同时在线,也不需要知道对方的存在,实现了通信的"解耦"。
2、MQ有哪些协议及对应产品?
(1) AMQP (Advanced Message Queuing Protocol)
-
特点:这是一种企业级的通用协议,功能非常强大。它支持复杂的路由规则(通过交换机 Exchange)、消息持久化、事务处理和可靠投递。它的设计目标是保证消息在不同厂商的系统间能可靠传输。
-
对应产品:
- RabbitMQ:最典型的代表,它是AMQP协议的完整实现,以其灵活的路由功能著称。
- ActiveMQ:老牌的消息中间件,对AMQP也有很好的支持。
- Qpid:Apache下的一个项目,也是AMQP的早期实现。
(2) MQTT (Message Queuing Telemetry Transport)
-
特点 :专为物联网 (IoT) 设计。它非常轻量级,占用带宽极低,基于"发布/订阅"模式。它非常适合网络不稳定或设备资源受限(如电池供电、低算力)的场景。
-
对应产品:
- EMQX (原EMQ):高性能的开源MQTT Broker,广泛用于物联网。
- Mosquitto:Eclipse旗下的轻量级开源MQTT Broker。
- HiveMQ:基于Java的企业级MQTT平台。
- 腾讯云 TDMQ MQTT版:云厂商提供的托管服务,常用于车联网、智能家居。
(3) JMS (Java Message Service)
-
特点 :严格来说,JMS 是 Java 平台定义的一套 API 规范(接口),而不是具体的网络传输协议。但它定义了消息中间件在 Java 世界里的标准用法(如点对点、发布订阅)。
-
对应产品:
- ActiveMQ:JMS 规范的经典实现。
- IBM MQ:金融领域常用的商业软件,完美支持 JMS。
- RocketMQ:虽然有自己的私有协议,但也提供了 JMS 的客户端支持,方便 Java 应用接入。
(4) STOMP (Simple Text Oriented Messaging Protocol)
-
特点:一种基于文本的简单协议,类似 HTTP。它的设计初衷是让任何支持 TCP 的语言都能轻松与消息中间件交互,不需要复杂的二进制解析。
-
对应产品:
- RabbitMQ 和 ActiveMQ 都内置了对 STOMP 的支持,常用于简单的脚本语言或前端 WebSocket 直接连接 MQ 的场景。
(5) 📊 自定义/专用协议
-
特点:为了追求极致的性能(高吞吐),很多现代 MQ 产品使用自定义的二进制协议,通常兼容 Kafka 协议。
-
对应产品:
- Kafka:使用自定义的基于 TCP 的二进制协议,强调高吞吐和分区日志存储。
- RocketMQ:使用自定义协议,但也兼容 OpenMessaging 标准。
- Pulsar:支持多种协议,但核心使用其自定义的二进制协议(Pulsar Protocol)。
3、为什么要用消息队列(作用)?
消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。
4、RabbitMQ
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
rabbitMQ是一款基于AMQP协议的消息中间件,它能够在应用之间提供可靠的消息传输。在易用性,扩展性,高可用性上表现优秀。使用消息中间件利于应用之间的解耦,生产者(客户端)无需知道消费者(服务端)的存在。而且两端可以使用不同的语言编写,大大提供了灵活性。
5、RabbitMQ的安装
- 服务端:
bash
安装配置epel源
$ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
安装erlang
$ yum -y install erlang
安装RabbitMQ
$ yum -y install rabbitmq-server
启动(无用户密码)
service rabbitmq-server start/stop
设置用户密码
sudo rabbitmqctl add_user username password
# 设置用户为administrator角色
sudo rabbitmqctl set_user_tags username administrator
# 设置权限
sudo rabbitmqtcl set_permissions -p "/" root ".*" ".*" ".*"
service rabbitmq-server start/stop
-
客户端:
pip3 install pika
6、使用
6.1 简单模式
ini
# ================ 生产者==================
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
channel.basic_publish(exchange='',
routing_key='hello',
body='Hello World!')
print(" [x] Sent 'Hello World!'")
# =====================消费者===============
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
channel.basic_consume(queue='hello',
auto_ack=True,
on_message_callback=callback)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
6.2 参数
- 应答参数ack(确保消费方接收安全)
ini
auto_ack=False
ch.basic_ack(delivery_tag=method.delivery_tag)
- 持久化参数durable(确保生产者数据安全)
ini
#声明queue
channel.queue_declare(queue='hello2', durable=True) # 若声明过,则换一个名字
channel.basic_publish(exchange='',
routing_key='hello2',
body='Hello World!',
properties=pika.BasicProperties(
delivery_mode=2, # make message persistent
)
)
- 分发参数prefetch_count
有两个消费者同时监听一个的队列。其中一个线程sleep2秒,另一个消费者线程sleep1秒,但是处理的消息是一样多。这种方式叫轮询分发(round-robin)不管谁忙,都不会多给消息,总是你一个我一个。想要做到公平分发(fair dispatch),必须关闭自动应答ack,改成手动应答。使用basicQos(perfetch=1)限制每次只发送不超过1条消息到同一个消费者,消费者必须手动反馈告知队列,才会发送下一个。
ini
channel.basic_qos(prefetch_count=1)
6.3 交换机模式
交换机之发布订阅
发布订阅和简单的消息队列区别在于,发布订阅会将消息发送给所有的订阅者,而消息队列中的数据被消费一次便消失。所以,RabbitMQ实现发布和订阅时,会为每一个订阅者创建一个队列,而发布者发布消息时,会将消息放置在所有相关队列中。
ini
# 生产者
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
exchange_type='fanout')
message = "info: Hello World!"
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
print(" [x] Sent %r" % message)
connection.close()
# 消费者
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
exchange_type='fanout')
result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='logs',
queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(queue=queue_name,
auto_ack=True,
on_message_callback=callback)
channel.start_consuming()
交换机之关键字

ini
# 生产者
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs2',
exchange_type='direct')
message = "info: Hello Yuan!"
channel.basic_publish(exchange='logs2',
routing_key='info',
body=message)
print(" [x] Sent %r" % message)
connection.close()
# 消费者
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs2',
exchange_type='direct')
result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue
severities = sys.argv[1:]
if not severities:
sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0])
sys.exit(1)
for severity in severities:
channel.queue_bind(exchange='logs2',
queue=queue_name,
routing_key=severity)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(queue=queue_name,
auto_ack=True,
on_message_callback=callback)
channel.start_consuming()
交换机之通配符
通配符交换机"与之前的路由模式相比,它将信息的传输类型的key更加细化,以"key1.key2.keyN...."的模式来指定信息传输的key的大类型和大类型下面的小类型,让消费者可以更加精细的确认自己想要获取的信息类型。而在消费者一段,不用精确的指定具体到哪一个大类型下的小类型的key,而是可以使用类似正则表达式(但与正则表达式规则完全不同)的通配符在指定一定范围或符合某一个字符串匹配规则的key,来获取想要的信息。
"通配符交换机"(Topic Exchange)将路由键和某模式进行匹配。此时队列需要绑定在一个模式上。符号"#"匹配一个或多个词,符号""仅匹配一个词。因此"audit.#"能够匹配到"audit.irs.corporate",但是"audit."只会匹配到"audit.irs"。(这里与我们一般的正则表达式的"*"和"#"刚好相反,这里我们需要注意一下。)
ini
# 生产者
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs3',
exchange_type='topic')
message = "info: Hello ERU!"
channel.basic_publish(exchange='logs3',
routing_key='europe.weather',
body=message)
print(" [x] Sent %r" % message)
connection.close()
# 消费者
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs3',
exchange_type='topic')
result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='logs3',
queue=queue_name,
routing_key="#.news")
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(" [x] %r" % body)
channel.basic_consume(queue=queue_name,
auto_ack=True,
on_message_callback=callback)
channel.start_consuming()
7、常见面试题
基础概念篇
1. 什么是消息中间件(MQ)?为什么要使用它?
消息中间件(Message Queue, MQ)是分布式系统中用于应用间异步通信的中间件。它通过"消息存储+转发"的模式,让服务之间不再直接强依赖调用。
引入MQ主要为了解决三大核心问题:
- 异步处理 (Asynchronous) :将非核心、耗时的操作(如发送邮件、记录日志)异步化,让主流程快速响应,提升用户体验。
- 应用解耦 (Decoupling) :服务A只需将消息发送到MQ,无需关心谁消费、如何消费。下游服务B、C、D可以独立地订阅和处理消息,系统扩展和维护更加灵活。
- 流量削峰 (Traffic Shaping) :在秒杀、大促等流量洪峰场景下,MQ可以作为缓冲区,将所有请求先接收并排队,后端服务再按照自己的处理能力匀速消费,避免系统被瞬间击垮。
2. 消息队列有哪些核心模型?
主要有两种核心通信模型:
- 点对点模型 (P2P / Queue) :一条消息只能被一个消费者消费。消息被消费后就会从队列中移除。适用于任务分发场景。
- 发布/订阅模型 (Pub/Sub / Topic) :一条消息可以被多个订阅了该主题的消费者同时消费。适用于广播通知场景。
核心挑战与解决方案篇
这部分是面试的重中之重,考察你对MQ深层次问题的理解和解决能力。
1. 如何保证消息不丢失?(可靠性)
要保证消息的可靠性,需要从生产者、MQ Broker、消费者三个环节入手,形成闭环。
- 生产者端 :开启消息确认机制 (如RabbitMQ的
publisher confirm,Kafka的acks=all)。确保消息已成功发送到MQ Broker,如果发送失败则进行重试。 - MQ Broker端 :开启消息持久化。将消息写入磁盘,防止MQ服务重启或宕机导致内存中的消息丢失。同时,可以采用主从复制(如RocketMQ、Kafka)来保证高可用。
- 消费者端 :关闭自动确认,采用手动ACK(Acknowledgment)。只有在业务逻辑成功处理完毕后,再向MQ发送确认信号。如果处理失败,可以返回NACK让消息重新入队或进入死信队列。
2. 如何保证消息不被重复消费?(幂等性)
网络抖动等原因可能导致同一条消息被投递多次。解决这个问题的核心是保证消费者接口的幂等性,即无论一个操作被执行多少次,其结果都是一致的。
常见的幂等性方案有:
- 数据库唯一约束:将消息的业务唯一ID(如订单号)作为数据库表的主键或唯一索引。重复插入时会因违反唯一约束而失败,直接忽略即可。
- Redis分布式锁 :在处理消息前,先以消息ID为Key在Redis中尝试获取锁(
SETNX命令)。如果获取成功则处理业务,处理完再释放锁;如果获取失败,说明该消息正在被处理或已处理过,直接跳过。 - 状态机判断 :在更新数据时带上状态条件。例如,更新订单状态时,SQL写成
UPDATE order SET status = 'PAID' WHERE id = ? AND status = 'UNPAID',这样即使重复执行,也只会成功一次。
3. 如何保证消息的顺序性?
在某些场景下(如订单的创建->支付->发货),消息必须按顺序处理。保证顺序的核心思路是将需要保证顺序的消息路由到同一个队列/分区,并保证单线程消费。
- 生产者端 :发送消息时,指定一个分区键(Partition Key) ,例如订单ID。MQ会根据这个Key进行哈希,确保同一个订单的所有消息都进入同一个队列/分区。
- 消费者端 :对于同一个队列/分区,必须使用单线程进行消费。如果开启多线程并发消费,就无法保证消息的处理顺序。
4. 消息大量堆积了怎么办?
消息堆积意味着消费者的处理速度跟不上生产者的生产速度,是一个需要紧急处理的线上问题。
-
紧急扩容:
- 增加消费者实例:快速部署更多的消费者服务,提升整体消费能力。
- 增加队列/分区数:如果当前队列数少于消费者数,可以先临时增加队列/分区,再增加消费者,实现水平扩展。
-
优化消费逻辑:排查消费者代码,优化耗时的操作,如慢SQL查询、外部接口调用等,提升单个消费者的处理效率。
-
降级处理:如果消息不是特别重要,可以考虑暂时丢弃或写入一个临时的存储(如数据库),待高峰期过后再进行异步处理。
主流产品选型篇
Kafka、RocketMQ、RabbitMQ 该如何选择?
这三款是目前最主流的MQ产品,它们的侧重点不同,适用于不同的业务场景。
| 产品 | 核心特点 | 典型应用场景 |
|---|---|---|
| RabbitMQ | 功能全面,路由灵活,社区成熟,可靠性高。 | 中小规模应用,对消息路由、延迟队列、可靠性要求高的场景。 |
| RocketMQ | 阿里开源,高吞吐,支持事务消息、顺序消息,金融级可靠。 | 电商交易、金融支付、物流等核心业务链路,需要高可靠和海量堆积能力。 |
| Kafka | 极高的吞吐量,性能卓越,生态完善,持久化能力强。 | 大数据日志收集、用户行为追踪、实时流计算(如Flink/Spark)。 |
一句话选型建议:
- 做大数据、日志处理 ,首选 Kafka。
- 做电商、金融等核心业务 ,追求高可靠和功能丰富,首选 RocketMQ。
- 做中小型项目 ,需要灵活的路由和快速上手,首选 RabbitMQ。