2. 消息队列基础

消息队列的模型,说白了就两种:点对点和发布订阅。我见过不少新手把这两个搞混,结果系统上线后数据对不上,排查起来特别痛苦。今天咱们就把这两个模型掰开揉碎,再把消息确认机制讲透。

点对点模型(P2P)

点对点模型,也叫队列模型。一条消息只能被一个消费者消费。消费完了,消息就从队列里消失。

核心特征: 一对一竞争消费,消息被消费后即删除。

我在项目中遇到过这样一个场景:订单系统处理支付回调,多个消费者实例同时监听同一个队列。如果不用点对点模型,同一个订单就会被重复处理,后果就是多发货、多退款。点对点模型天然解决了这个问题——一条消息只会被其中一个消费者拿走。

特性 说明
消息路由 直接投递到队列,无路由规则
消费关系 1条消息 → 1个消费者
消息生命周期 消费即删除,不支持回溯
典型场景 任务分发、日志采集、异步处理
// 点对点模型示例(伪代码)
// 生产者
Queue queue = channel.declareQueue("order.payment.queue");
queue.publish("payment_success", orderId);

// 消费者(竞争消费)
queue.consume(message -> {
    processPayment(message.getOrderId());
    message.ack();  // 确认消费
});
我的习惯: 点对点队列的消费者数量建议等于分区数。多了浪费资源,少了处理不过来。我曾经因为消费者数设少了,导致消息积压了 20 万条,排查了整整一个下午。

发布订阅模型(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();
});
注意: 发布订阅模式下,如果某个消费者挂了,消息不会重试。需要结合死信队列或重试机制来兜底。我曾经因为这个踩过坑,一个消费者宕机了 3 分钟,丢了 2000 条注册事件,用户投诉电话被打爆了。

消息确认机制

消息确认机制,是消息队列的命门。没有它,消息可能丢,也可能重复处理。我把它分成三个层次来讲。

生产端确认

生产者发完消息,怎么知道 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);

死信队列的作用就是兜底。我习惯给每个业务队列配一个死信队列,再写一个监控脚本,死信队列有消息就报警。这样即使出了问题,也能第一时间发现,而不是等用户投诉了才去查。

避坑指南: 我曾经把重试次数设成了 10 次,结果某个下游服务挂了,消息在队列里反复重试,把数据库连接池打满了。现在我的经验是:重试 3 次,间隔指数退避,3 次后直接进死信队列,人工介入处理。

三种模型的关系图

下面这张图把点对点、发布订阅和确认机制串在一起,方便你理解整体架构。

消息队列核心模型关系图 消息队列核心模型关系图 生产者 点对点队列 发布订阅Topic 消费者组(竞争消费) 消费者A 消费者B 订阅者列表(广播) 订阅者A 订阅者B 消息确认机制 点对点:一条消息只被一个消费者消费 | 发布订阅:一条消息被所有订阅者消费 | 确认机制:保证消息不丢不重

如何选择模型

我总结了一个简单的选择原则:

  • 任务分发、负载均衡 → 点对点模型
  • 事件通知、广播、数据同步 → 发布订阅模型
  • 要求高可靠性、不丢消息 → 手动确认 + 死信队列
  • 追求高吞吐、允许少量丢消息 → 自动确认 + 批量发送
重要提醒: 不要为了省事把所有场景都用同一种模型。我见过一个团队,所有消息都用发布订阅,结果日志采集和订单处理混在一起,排查问题的时候根本分不清消息是谁发的。该用点对点就用点对点,该用发布订阅就用发布订阅,别混着来。

嗯,以上就是消息队列的三大核心模型。理解透这些,后面讲性能调优和故障排查的时候,你才能知道问题出在哪个环节。记住:模型选对了,架构就稳了一半。