消息队列核心模型:点对点、发布订阅与确认机制
消息队列的模型,说白了就两种:点对点和发布订阅。我见过不少新手把这两个搞混,结果系统上线后数据对不上,排查起来特别痛苦。今天咱们就把这两个模型掰开揉碎,再把消息确认机制讲透。
点对点模型(P2P)
点对点模型,也叫队列模型。一条消息只能被一个消费者消费。消费完了,消息就从队列里消失。
我在项目中遇到过这样一个场景:订单系统处理支付回调,多个消费者实例同时监听同一个队列。如果不用点对点模型,同一个订单就会被重复处理,后果就是多发货、多退款。点对点模型天然解决了这个问题——一条消息只会被其中一个消费者拿走。
| 特性 | 说明 |
|---|---|
| 消息路由 | 直接投递到队列,无路由规则 |
| 消费关系 | 1条消息 → 1个消费者 |
| 消息生命周期 | 消费即删除,不支持回溯 |
| 典型场景 | 任务分发、日志采集、异步处理 |
// 点对点模型示例(伪代码)
// 生产者
Queue queue = channel.declareQueue("order.payment.queue");
queue.publish("payment_success", orderId);
// 消费者(竞争消费)
queue.consume(message -> {
processPayment(message.getOrderId());
message.ack(); // 确认消费
});
发布订阅模型(Pub/Sub)
发布订阅模型就完全不一样了。一条消息可以被多个消费者同时消费。生产者把消息发到 Topic,所有订阅了这个 Topic 的消费者都能收到。
你想想看,这不就是广播吗?对,本质上就是广播。我做过一个实时风控系统,用户每笔交易都要同时触发:规则引擎、行为分析、通知推送、数据归档。如果用点对点模型,得发 4 次消息。用发布订阅,一次发布,四个消费者各自处理,干净利落。
| 特性 | 说明 |
|---|---|
| 消息路由 | 通过 Topic 路由,支持通配符 |
| 消费关系 | 1条消息 → N个消费者 |
| 消息生命周期 | 所有消费者消费完才删除 |
| 典型场景 | 事件通知、数据同步、广播 |
// 发布订阅模型示例(伪代码)
// 生产者
Topic topic = channel.declareTopic("user.register");
topic.publish("user_registered", userInfo);
// 消费者A:发送欢迎邮件
topic.subscribe("email_service", message -> {
sendWelcomeEmail(message.getUserEmail());
message.ack();
});
// 消费者B:初始化用户空间
topic.subscribe("space_service", message -> {
initUserSpace(message.getUserId());
message.ack();
});
消息确认机制
消息确认机制,是消息队列的命门。没有它,消息可能丢,也可能重复处理。我把它分成三个层次来讲。
生产端确认
生产者发完消息,怎么知道 Broker 收到了?
- 同步确认: 发一条等一条,吞吐量低但可靠
- 异步确认: 批量发送,回调通知,吞吐量高
- 事务消息: 两阶段提交,保证生产者和 Broker 状态一致
// 生产端确认示例
// 同步确认
channel.confirmSelect();
channel.basicPublish("exchange", "routingKey", message);
if (channel.waitForConfirms()) {
// 消息已到达 Broker
} else {
// 消息丢失,需要重发
}
// 异步确认
channel.addConfirmListener((sequenceNumber, multiple) -> {
// 确认成功
}, (sequenceNumber, multiple) -> {
// 确认失败,记录日志并重试
});
消费端确认
消费者拿到消息,处理完了,得告诉 Broker 一声。这里有两种模式:
| 模式 | 行为 | 风险 |
|---|---|---|
| 自动确认 | 消费者收到消息即确认 | 消费者处理过程中挂了,消息丢失 |
| 手动确认 | 消费者处理完业务逻辑后才确认 | 处理时间过长,消息可能被重新投递 |
重试与死信
消息处理失败怎么办?重试。重试还失败呢?进死信队列。
// 死信队列配置示例
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "dlx.exchange");
args.put("x-dead-letter-routing-key", "dlx.routingKey");
args.put("x-message-ttl", 60000); // 消息存活时间
Queue queue = channel.declareQueue("business.queue", args);
死信队列的作用就是兜底。我习惯给每个业务队列配一个死信队列,再写一个监控脚本,死信队列有消息就报警。这样即使出了问题,也能第一时间发现,而不是等用户投诉了才去查。
三种模型的关系图
下面这张图把点对点、发布订阅和确认机制串在一起,方便你理解整体架构。
如何选择模型
我总结了一个简单的选择原则:
- 任务分发、负载均衡 → 点对点模型
- 事件通知、广播、数据同步 → 发布订阅模型
- 要求高可靠性、不丢消息 → 手动确认 + 死信队列
- 追求高吞吐、允许少量丢消息 → 自动确认 + 批量发送
嗯,以上就是消息队列的三大核心模型。理解透这些,后面讲性能调优和故障排查的时候,你才能知道问题出在哪个环节。记住:模型选对了,架构就稳了一半。