📖 开场:餐厅的两种服务模式
想象你去餐厅吃饭 🍽️:
推模式(Push)- 服务员主动上菜:
厨房做好菜 → 服务员立即端到你桌上 🏃
↓
优点:实时性好,菜做好立即送达 ⚡
缺点:如果你吃不下了,服务员还在上菜 😰
拉模式(Pull)- 你主动叫服务员:
厨房做好菜 → 放在出餐口 → 你喊"服务员!" → 服务员端菜
↓
优点:你能吃多快就拿多快,不会撑着 😊
缺点:需要你主动叫,可能不够及时 🐌
这就是消息队列的推拉模式!
🎯 什么是推拉模式?
Push模式(推模式)
定义:Broker主动将消息推送给Consumer
┌─────────┐
│ Broker │
└────┬────┘
│ 主动推送消息
↓
┌─────────┐
│Consumer │ ← 被动接收
└─────────┘
特点:
- Broker负责推送
- Consumer被动接收
- 实时性好
代表:RabbitMQ、RocketMQ(Push Consumer)
Pull模式(拉模式)
定义:Consumer主动从Broker拉取消息
┌─────────┐
│ Broker │ ← 被动等待
└────┬────┘
↑ 主动拉取消息
┌─────────┐
│Consumer │
└─────────┘
特点:
- Consumer主动拉取
- Broker被动等待
- Consumer可控性强
代表:Kafka、RocketMQ(Pull Consumer)
📊 Push vs Pull 详细对比
1️⃣ 实时性对比
Push模式 ⚡⚡⚡
消息到达Broker → 立即推送给Consumer
延迟:<10ms
优势:
- ✅ 实时性极好
- ✅ 消息一到立即处理
- ✅ 适合实时性要求高的场景
示例(RocketMQ Push):
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("push_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("OrderTopic", "*");
// ⭐ Broker会主动推送消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeConcurrentlyContext context
) {
// 消息来了,立即处理!
for (MessageExt msg : msgs) {
System.out.println("收到消息: " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
Pull模式 😐
Consumer定时拉取(如每100ms拉一次)
延迟:0-100ms
劣势:
- ❌ 有轮询间隔,实时性稍差
- ❌ 频繁拉取浪费资源
- ❌ 拉取间隔过长,延迟高
示例(Kafka Pull):
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("orders"));
while (true) {
// ⭐ 主动拉取消息(每100ms拉一次)
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.println("拉取到消息: " + record.value());
}
}
优化:长轮询(Long Polling):
Consumer拉取时,如果没有消息:
- 不立即返回空结果
- 而是在Broker端等待(如30秒)
- 一旦有消息,立即返回
效果:实时性接近Push,但保持Pull的优势 ⭐
2️⃣ 流量控制对比
Push模式 😰
Broker疯狂推送 → Consumer处理不过来 → 消息堆积在Consumer内存 💀
问题:
- ❌ Consumer容易被压垮
- ❌ Consumer处理慢,Broker还在推
- ❌ 内存溢出风险
- ❌ 需要复杂的流控机制
场景:
Broker推送速度:10000条/秒 🚀
Consumer处理速度:1000条/秒 🐌
结果:
Consumer内存堆积9000条/秒
10秒后:90000条消息堆积 💀
100秒后:OOM(内存溢出)!💀💀💀
RabbitMQ的流控:
// 设置预取数量(限流)
channel.basicQos(10); // ⭐ 一次最多推送10条
// Consumer确认消费后,才会推送下一批
channel.basicConsume(queueName, false, consumer);
图示:
Broker Consumer
│ │
├──推送10条消息────────────────>│
│ ├─ 处理中...
│ │
│ 等待Consumer确认... │
│ │
│<──────ACK确认─────────────────┤
│ │
├──推送下一批10条───────────────>│
Pull模式 ✅
Consumer根据自己的处理能力,主动拉取 😊
优势:
- ✅ Consumer完全可控
- ✅ 吃得下多少拉多少
- ✅ 不会被压垮
- ✅ 天然的流量控制
示例(Kafka):
props.put("max.poll.records", 100); // ⭐ 每次最多拉100条
while (true) {
// 主动拉取,拉多少自己决定
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
// 处理100条消息
for (ConsumerRecord<String, String> record : records) {
processMessage(record); // 处理慢也没关系,下次少拉点
}
// 处理完才拉下一批
consumer.commitSync();
}
对比:
Push模式:
Broker:1万条/秒 🚀
Consumer:1千条/秒 🐌
→ Consumer崩溃 💀
Pull模式:
Consumer:我只拉100条 ✅
处理完再拉下一批 ✅
→ Consumer稳定运行 😊
3️⃣ 消费速率对比
Push模式
Broker决定推送速率:
Broker:我要推了!
Consumer:等等,我还没处理完...
Broker:不管,继续推!
Consumer:💀
需要配合限流:
// RabbitMQ
channel.basicQos(10); // 限制推送数量
// RocketMQ
consumer.setConsumeMessageBatchMaxSize(10); // 限制批量大小
consumer.setConsumeThreadMin(5); // 消费线程数
consumer.setConsumeThreadMax(10);
Pull模式 ⭐
Consumer决定拉取速率:
Consumer:我处理得快,拉500条!
Consumer:我处理得慢,拉10条!
Consumer:我现在很忙,不拉了!
灵活调节:
// 根据当前负载动态调整
int batchSize = getCurrentLoad() < 50 ? 500 : 100;
props.put("max.poll.records", batchSize);
4️⃣ 复杂度对比
Push模式 😰
Broker端复杂:
- ❌ 需要管理每个Consumer的连接
- ❌ 需要维护推送状态
- ❌ 需要流控机制
- ❌ 需要处理Consumer离线/在线
Consumer端简单:
- ✅ 只需注册监听器
- ✅ 被动接收即可
Pull模式 😊
Broker端简单:
- ✅ 只需存储消息
- ✅ 等待Consumer拉取
- ✅ 无需管理连接状态
Consumer端稍复杂:
- ❌ 需要主动拉取
- ❌ 需要管理offset
- ❌ 需要处理空闲
5️⃣ 可靠性对比
Push模式
问题场景:
Broker推送消息 → 网络故障 → 消息丢失 💀
Broker推送消息 → Consumer宕机 → 消息丢失 💀
需要ACK机制:
// RabbitMQ手动确认
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(...) {
try {
// 处理消息
processMessage(body);
// ⭐ 手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// ⭐ 拒绝消息,重新入队
channel.basicNack(envelope.getDeliveryTag(), false, true);
}
}
});
Pull模式 ✅
更可靠:
Consumer拉取消息 → 处理成功 → 提交offset ✅
Consumer拉取消息 → 处理失败 → 不提交offset,下次重新拉取 ✅
示例:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
try {
processMessage(record);
// ⭐ 处理成功,提交offset
consumer.commitSync();
} catch (Exception e) {
// ⭐ 处理失败,不提交,下次重新拉取
log.error("处理失败", e);
break;
}
}
}
6️⃣ 扩展性对比
Push模式 😐
扩展限制:
Broker需要维护所有Consumer的连接
1万个Consumer = 1万个连接 😰
10万个Consumer = 10万个连接 💀
Broker压力大!
Pull模式 ✅
易于扩展:
Consumer无状态,随意增减
Broker只需存储消息,无需管理连接
1万个Consumer = OK ✅
10万个Consumer = OK ✅
100万个Consumer = OK ✅
📊 对比总结表
| 特性 | Push模式 | Pull模式 |
|---|---|---|
| 实时性 | ⚡⚡⚡ 极好(<10ms) | 😐 一般(0-100ms),长轮询可优化 |
| 流量控制 | 😰 复杂(需要流控机制) | ✅ 简单(Consumer自控) |
| 消费速率 | 😐 Broker决定 | ✅ Consumer决定 |
| Broker复杂度 | 😰 高(管理连接) | ✅ 低(只存储) |
| Consumer复杂度 | ✅ 低(被动接收) | 😐 中(主动拉取) |
| 可靠性 | 😐 需要ACK机制 | ✅ 更可靠(处理后才提交) |
| 扩展性 | 😐 有限制 | ✅ 易扩展 |
| 代表 | RabbitMQ | Kafka ⭐ |
🔧 实际应用案例
Kafka的Pull模式
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-consumer-group");
props.put("enable.auto.commit", false); // 手动提交
props.put("max.poll.records", 500); // 每次拉取500条
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("orders"));
while (true) {
// ⭐ 主动拉取消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
System.out.println("拉取到 " + records.count() + " 条消息");
for (ConsumerRecord<String, String> record : records) {
System.out.printf("消费: partition=%d, offset=%d, key=%s, value=%s%n",
record.partition(), record.offset(), record.key(), record.value());
processMessage(record);
}
// ⭐ 处理完才提交offset
consumer.commitSync();
}
优势:
- Consumer可以根据自己的处理能力调整拉取数量
- 处理慢了,少拉点;处理快了,多拉点
- 不会被Broker压垮
RabbitMQ的Push模式
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queueName = "orders";
channel.queueDeclare(queueName, true, false, false, null);
// ⭐ 限流:每次最多推10条
channel.basicQos(10);
// ⭐ 注册监听器,被动接收
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
try {
String message = new String(body, "UTF-8");
System.out.println("收到推送消息: " + message);
processMessage(message);
// ⭐ 手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
try {
// ⭐ 拒绝消息,重新入队
channel.basicNack(envelope.getDeliveryTag(), false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
});
优势:
- 实时性好,消息一到立即推送
- Consumer实现简单,只需注册监听器
RocketMQ:同时支持Push和Pull
Push Consumer(实际上是长轮询Pull)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("push_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("OrderTopic", "*");
// ⭐ 看起来是Push,实际是长轮询Pull
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeConcurrentlyContext context
) {
for (MessageExt msg : msgs) {
System.out.println("消费: " + new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
原理:
底层实现:
1. Consumer主动拉取消息(Pull)
2. 如果没有消息,Broker端等待30秒(长轮询)
3. 一旦有消息,立即返回
4. Consumer收到消息,调用监听器
效果:既有Push的实时性,又有Pull的优势!⭐⭐⭐⭐⭐
Pull Consumer(手动拉取)
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.start();
// 获取MessageQueue
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("OrderTopic");
for (MessageQueue mq : mqs) {
long offset = consumer.fetchConsumeOffset(mq, false);
while (true) {
// ⭐ 主动拉取消息
PullResult pullResult = consumer.pull(mq, "*", offset, 32);
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> msgs = pullResult.getMsgFoundList();
for (MessageExt msg : msgs) {
System.out.println("拉取到消息: " + new String(msg.getBody()));
}
offset = pullResult.getNextBeginOffset();
break;
case NO_NEW_MSG:
System.out.println("没有新消息");
break;
case NO_MATCHED_MSG:
System.out.println("没有匹配的消息");
break;
case OFFSET_ILLEGAL:
System.out.println("Offset非法");
break;
}
// 更新offset
consumer.updateConsumeOffset(mq, offset);
}
}
🎯 如何选择?
选择Push模式的场景
适用:
- ✅ 实时性要求极高(<10ms)
- ✅ Consumer处理能力稳定
- ✅ 消息量不大
- ✅ 需要简单的Consumer实现
例子:
- 即时通讯(IM)
- 实时监控告警
- 股票行情推送
选择Pull模式的场景 ⭐⭐⭐⭐⭐
适用:
- ✅ 消息量大,需要批量处理
- ✅ Consumer处理能力不稳定
- ✅ 需要流量控制
- ✅ 需要高可靠性
例子:
- 订单处理
- 日志收集
- 数据同步
- 大数据处理
最佳方案:长轮询Pull ⭐⭐⭐⭐⭐
结合Push和Pull的优点:
// Kafka的长轮询
props.put("fetch.min.bytes", 1); // 至少1字节
props.put("fetch.max.wait.ms", 30000); // 最多等待30秒
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(30000));
工作原理:
1. Consumer发起拉取请求
2. Broker如果有消息:立即返回 ⚡
3. Broker如果没消息:等待30秒 ⏰
4. 30秒内有新消息:立即返回 ⚡
5. 30秒后仍无消息:返回空结果
效果:
- 实时性好(有消息立即返回)✅
- 不浪费资源(无消息时等待)✅
- Consumer可控(主动拉取)✅
图示:
Consumer Broker
│ │
├──Pull请求──────────>│
│ │ 没有消息,等待...
│ │
│ │ 新消息到达!
│ │
│<─────返回消息────────┤
│ │
├──处理消息────────────│
│ │
├──Pull请求──────────>│
│ │ 立即有消息
│<─────返回消息────────┤
🎓 面试题速答
Q1: Push和Pull模式的区别?
A: Push(推模式):
- Broker主动推送消息给Consumer
- 实时性好(<10ms)
- Consumer容易被压垮
- 代表:RabbitMQ
Pull(拉模式):
- Consumer主动从Broker拉取消息
- Consumer可控性强
- 天然流量控制
- 代表:Kafka ⭐
Q2: 为什么Kafka选择Pull模式?
A: 三个原因!
- 流量控制:Consumer根据自己的处理能力拉取,不会被压垮
- 批量处理:一次可以拉取多条消息,提高吞吐量
- 简化Broker:Broker无需管理Consumer状态,易扩展
补充:Kafka使用长轮询优化实时性
Q3: Push模式如何防止Consumer被压垮?
A: 限流机制:
RabbitMQ:
channel.basicQos(10); // 每次最多推10条
RocketMQ:
consumer.setConsumeMessageBatchMaxSize(10); // 批量大小
consumer.setConsumeThreadMin(5); // 消费线程数
原理:
- Consumer未确认前,Broker不推送新消息
- 类似TCP的滑动窗口机制
Q4: 什么是长轮询?
A: 长轮询(Long Polling) = Pull + 等待
流程:
1. Consumer拉取消息
2. Broker如果有消息:立即返回
3. Broker如果无消息:等待(如30秒)
4. 等待期间有新消息:立即返回
5. 超时仍无消息:返回空
效果:既有Push的实时性,又有Pull的优势!⭐
Kafka配置:
props.put("fetch.max.wait.ms", 30000); // 最多等待30秒
Q5: RocketMQ的PushConsumer是真的Push吗?
A: 不是!是长轮询Pull!
原理:
1. PushConsumer底层是PullConsumer
2. 内部自动长轮询拉取消息
3. 拉到消息后,调用监听器
4. 对用户屏蔽了Pull细节,看起来像Push
效果:用起来像Push,底层是Pull!⭐⭐⭐⭐⭐
Q6: 如何选择Push还是Pull?
A: 选Pull!(大部分场景)⭐⭐⭐⭐⭐
原因:
- Consumer可控性强
- 天然流量控制
- 易扩展
- 长轮询可以优化实时性
选Push的场景(少数):
- 实时性要求极高(<10ms)
- 消息量很小
- Consumer处理能力稳定
🎯 最佳实践
Kafka消费者最佳实践
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-consumer-group");
props.put("enable.auto.commit", false);
// ⭐ 批量拉取配置
props.put("max.poll.records", 500); // 每次拉取500条
props.put("fetch.min.bytes", 1024); // 至少1KB
props.put("fetch.max.wait.ms", 30000); // 最多等待30秒(长轮询)
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("orders"));
while (true) {
// 主动拉取
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
if (records.isEmpty()) {
continue; // 没有消息,继续拉取
}
System.out.println("拉取到 " + records.count() + " 条消息");
// 批量处理
List<Order> orders = new ArrayList<>();
for (ConsumerRecord<String, String> record : records) {
orders.add(parseOrder(record.value()));
}
// 批量插入数据库
orderRepository.batchInsert(orders);
// 处理成功,提交offset
consumer.commitSync();
}
优势:
- 批量拉取,批量处理
- 吞吐量高
- Consumer可控
RabbitMQ消费者最佳实践
Channel channel = connection.createChannel();
String queueName = "orders";
// ⭐ 限流:每次最多推送10条
channel.basicQos(10);
// 注册消费者
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
try {
String message = new String(body, "UTF-8");
processMessage(message);
// ⭐ 手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
log.error("处理失败", e);
try {
// ⭐ 拒绝并重新入队
channel.basicNack(envelope.getDeliveryTag(), false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
});
优势:
- 实时性好
- 手动确认保证可靠性
- 限流防止被压垮
🎬 总结
推拉模式对比总结图
┌──────────────────────────────────────────────┐
│ Push模式(推模式) │
│ │
│ 优点: │
│ ✅ 实时性极好(<10ms) │
│ ✅ Consumer实现简单 │
│ │
│ 缺点: │
│ ❌ Consumer容易被压垮 │
│ ❌ 需要复杂的流控机制 │
│ ❌ Broker管理复杂 │
│ │
│ 代表:RabbitMQ │
│ 适用:实时性要求极高的场景 │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Pull模式(拉模式)⭐⭐⭐⭐⭐ │
│ │
│ 优点: │
│ ✅ Consumer可控性强 │
│ ✅ 天然流量控制 │
│ ✅ 易扩展 │
│ ✅ 批量处理,高吞吐 │
│ │
│ 缺点: │
│ ❌ 实时性稍差(可用长轮询优化) │
│ │
│ 代表:Kafka │
│ 适用:大部分场景(推荐) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 长轮询Pull(最佳方案)🏆 │
│ │
│ 特点:结合Push和Pull的优点 │
│ ├─ 有消息:立即返回(实时性好) │
│ ├─ 无消息:等待30秒(不浪费资源) │
│ └─ Consumer主动拉取(可控性强) │
│ │
│ 代表:Kafka、RocketMQ PushConsumer │
└──────────────────────────────────────────────┘
推荐:长轮询Pull模式 🏆
🎉 恭喜你!
你已经完全掌握了消息队列的推拉模式!🎊
核心要点:
- Push:实时性好,但Consumer容易被压垮
- Pull:Consumer可控,天然流量控制(推荐)⭐
- 长轮询:结合两者优点,最佳方案 🏆
下次面试,这样回答:
"消息队列有Push和Pull两种模式。
Push模式是Broker主动推送消息,实时性好,但Consumer容易被压垮,需要复杂的流控机制,代表是RabbitMQ。
Pull模式是Consumer主动拉取消息,Consumer可控性强,天然流量控制,易扩展,代表是Kafka。
Kafka使用长轮询优化Pull模式:拉取时如果没有消息,Broker端等待30秒,一旦有新消息立即返回,既有Push的实时性,又有Pull的优势。
大部分场景推荐用Pull模式,特别是Kafka的长轮询Pull,性能和可控性都很好。"
面试官:👍 "很好!你对推拉模式理解很透彻!"
本文完 🎬
上一篇: 190-延迟消息的实现原理.md
下一篇: 192-死信队列的作用和处理策略.md
作者注:写完这篇,我去餐厅吃饭都知道叫服务员了!🍽️
如果这篇文章对你有帮助,请给我一个Star⭐!