1. 从一条消息到多个队列:为什么需要主题分发
消息中间件用久了,迟早会碰到一个绕不开的场景:业务系统产生了一条核心事件,比如订单创建成功,但下游有好几个模块都关心这件事——风控模块要实时评估风险,通知模块要给用户发短信,数据模块要落库做分析。如果让订单服务挨个去调用这些下游接口,代码耦合不说,任何一个下游挂了都会拖累主流程。这时候,消息队列的发布订阅能力就成了刚需。
RabbitMQ 作为老牌消息代理,处理这类需求的核心武器就是交换机(Exchange)。生产者只管把消息丢给交换机,交换机根据自己手里的路由规则,把消息复制分发到一个或多个队列里。每个队列背后挂一个消费者,各取所需,互不干扰。标题里说的“用接收器接受多个主题”,本质上就是利用Topic 类型的交换机配合通配符路由键,实现一条消息精准命中多个队列。
这套机制解决的是解耦和广播两个问题。解耦让生产者不需要知道有多少消费者、消费者是谁;广播让同一条消息能被多个互不相关的业务模块同时消费。适合谁看?如果你正在用 SpringBoot 做微服务,或者手头有个单体应用想拆出异步通知能力,又或者你被“一个事件多个下游”的需求折磨过,那这篇内容就是给你准备的。我会从交换机选型讲到代码落地,把路由键匹配、队列绑定、消息序列化这些坑一个个填平。
2. 整体设计思路与核心组件拆解
2.1 为什么是 Topic 交换机而不是 Fanout 或 Direct
RabbitMQ 自带四种交换机类型:Direct、Fanout、Topic、Headers。选哪种,取决于你对路由精度的要求。
Direct 交换机做的是精确匹配,路由键写死成order.create,那绑定的队列也只能收这个键。如果风控模块想收所有订单相关事件,通知模块只想收创建事件,Direct 就力不从心了,你得为每个组合单独建队列和绑定,维护成本爆炸。
Fanout 交换机走另一个极端,它无视路由键,把消息广播给所有绑定的队列。这适合做纯广播,比如配置刷新通知。但它的缺点是没法做筛选,所有队列都会收到全量消息,消费者得自己过滤,浪费资源。
Topic 交换机是两者的折中,也是实际项目里用得最多的。它支持通配符匹配:*匹配一个单词,#匹配零个或多个单词,单词之间用点号分隔。比如路由键order.create.success,绑定order.*.success能匹配,绑定order.#也能匹配,绑定order.create.*同样能匹配。这种灵活性让一条消息可以同时命中多个不同粒度的队列。
我选 Topic 的核心理由是:路由规则可以随业务演进。今天风控只关心创建事件,明天它想关心所有订单事件,我只需要改绑定键,不用动生产者的代码,也不用新建交换机。这种可扩展性在业务快速迭代期非常值钱。
2.2 核心组件与数据流向
整套方案涉及四个角色,我用一个订单场景串起来讲:
- 生产者(Producer):订单服务,产生消息,发送到名为
order.topic.exchange的 Topic 交换机,路由键设为order.create.success。 - 交换机(Exchange):
order.topic.exchange,负责根据路由键把消息路由到匹配的队列。 - 队列(Queue):建三个队列,分别是
queue.risk(风控)、queue.notify(通知)、queue.analytics(数据分析)。 - 消费者(Consumer):三个独立的监听器,分别监听三个队列。
绑定关系是这样的:
| 队列名 | 绑定键 | 匹配逻辑 |
|---|---|---|
| queue.risk | order.# | 匹配所有以 order 开头的路由键 |
| queue.notify | order.create.* | 匹配 order.create 后跟一个单词的路由键 |
| queue.analytics | order.*.success | 匹配 order 后跟一个单词再跟 success 的路由键 |
当生产者发送路由键为order.create.success的消息时,三个队列都会收到。如果发送order.cancel.success,风控和分析队列收到,通知队列收不到。如果发送order.create.fail,只有风控队列收到。这就是 Topic 交换机的威力。
2.3 SpringBoot 集成 RabbitMQ 的依赖与配置选型
SpringBoot 集成 RabbitMQ 靠的是spring-boot-starter-amqp这个 starter,它底层封装了 Spring AMQP 项目,提供了RabbitTemplate用于发送消息,@RabbitListener注解用于消费消息。版本上,SpringBoot 2.x 对应 Spring AMQP 2.x,SpringBoot 3.x 对应 Spring AMQP 3.x,后者要求 Java 17 起步,这个在选型时要留意。
配置层面,我习惯把交换机名、队列名、路由键这些常量抽到一个单独的类里,避免散落在各处。连接工厂用默认的CachingConnectionFactory就够了,它支持连接和通道的缓存,性能上没问题。消息序列化默认是 JDK 序列化,这玩意儿有个坑:生产者和消费者的类路径必须完全一致,否则反序列化失败。我一般会换成 Jackson 的Jackson2JsonMessageConverter,用 JSON 传输,跨语言、跨版本都友好。
提示:如果项目里已经有 Redis 的序列化配置,别直接复用,RabbitMQ 的 MessageConverter 是独立的,需要单独声明 Bean。
3. 核心细节解析与实操要点
3.1 交换机与队列的声明式配置
SpringBoot 里声明交换机、队列、绑定关系有两种方式:一种是纯 Java 配置类,用@Bean注解;另一种是在application.yml里配置,让 SpringBoot 自动声明。我推荐用 Java 配置类,因为绑定关系复杂的时候,YAML 写起来很痛苦,而且 Java 配置可以做条件判断,灵活性更高。
声明 Topic 交换机的代码如下:
@Bean public TopicExchange orderTopicExchange() { return new TopicExchange("order.topic.exchange", true, false); }三个参数分别是交换机名、是否持久化、是否自动删除。持久化设为 true,这样 RabbitMQ 重启后交换机还在。自动删除设为 false,避免没有队列绑定时交换机被删掉。
队列声明类似:
@Bean public Queue riskQueue() { return new Queue("queue.risk", true); }绑定关系用BindingBuilder来构建:
@Bean public Binding riskBinding() { return BindingBuilder.bind(riskQueue()) .to(orderTopicExchange()) .with("order.#"); }这里.with("order.#")就是绑定键,#是通配符。注意绑定键的写法要和生产者发送的路由键风格保持一致,都用点号分隔的小写单词,别一会儿驼峰一会儿下划线,后期排查问题会疯掉。
3.2 路由键通配符的匹配规则与常见误区
Topic 交换机的通配符规则看着简单,实际用起来有几个容易踩的坑。
第一个坑:*和#的区别。*必须匹配一个完整的单词,#可以匹配零个或多个。比如绑定键order.*,路由键order是匹配不上的,因为*要求至少有一个单词。但绑定键order.#,路由键order能匹配,因为#可以匹配零个单词。这个细节在调试时经常让人困惑。
第二个坑:单词分隔符只能是点号。路由键orderCreateSuccess和绑定键order.*是匹配不上的,因为没有点号分隔。所以命名规范一定要统一,我建议全小写加点号,比如order.create.success。
第三个坑:#放在中间的情况。绑定键order.#.success可以匹配order.success、order.create.success、order.a.b.success,这个特性在做多层级事件路由时很有用,但可读性会下降,团队里要约定好使用场景。
注意:路由键的长度没有硬性限制,但过长的路由键会影响匹配性能,建议控制在 255 字节以内,这是 AMQP 协议对短字符串的建议长度。
3.3 消息序列化与反序列化的正确姿势
前面提到默认的 JDK 序列化有类路径耦合问题,换成 JSON 序列化是更稳妥的做法。配置方式是在 RabbitListenerContainerFactory 和 RabbitTemplate 里都设置 MessageConverter。
@Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); }然后在RabbitTemplate的 Bean 里设置:
@Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); template.setMessageConverter(jsonMessageConverter()); return template; }消费者这边,如果用的是@RabbitListener注解,SpringBoot 会自动从容器工厂里取 MessageConverter,但前提是你自定义了SimpleRabbitListenerContainerFactory并设置了 converter。如果没设置,它还是用默认的 JDK 序列化,导致消费端报ClassNotFoundException或者MessageConversionException。
我踩过的一个坑是:生产者发了 JSON,消费者用@RabbitListener接收时方法参数写的是具体实体类,结果报转换异常。排查半天发现是容器工厂没配 converter。后来我养成了一个习惯,在配置类里显式声明容器工厂:
@Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( ConnectionFactory connectionFactory, MessageConverter jsonMessageConverter) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(jsonMessageConverter); return factory; }这样消费者端就能自动把 JSON 转成实体类了。
3.4 消费者并发与消息确认机制
默认情况下,每个@RabbitListener对应一个消费者线程,消息一条条处理。如果队列里积压了大量消息,单线程消费会成为瓶颈。Spring AMQP 提供了concurrency参数来设置并发消费者数量:
@RabbitListener(queues = "queue.risk", concurrency = "3-10")3-10表示最小 3 个消费者,最大 10 个,根据负载动态调整。这个参数要根据队列的吞吐量和消费者的处理耗时来定。如果消费者处理一条消息要 500ms,队列每秒进来 100 条,那至少需要 50 个消费者才能跟上,但实际中还要考虑数据库连接池、下游接口限流等因素,不能无脑调大。
消息确认机制方面,SpringBoot 默认是自动确认(AUTO),即消息一旦投递给消费者就立即确认,不管业务逻辑有没有执行成功。这在生产环境是危险的,如果消费者处理到一半挂了,消息就丢了。我建议改成手动确认:
spring: rabbitmq: listener: simple: acknowledge-mode: manual然后在消费者方法里注入Channel参数,业务处理成功后调用channel.basicAck(deliveryTag, false),失败时调用channel.basicNack(deliveryTag, false, true)让消息重新入队。这样能保证消息至少被处理一次,配合幂等设计就能做到不丢不重。
4. 完整实操过程与核心环节实现
4.1 项目依赖与基础配置
先建一个 SpringBoot 项目,pom.xml 里引入 AMQP starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>application.yml 里配置连接信息:
spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest virtual-host: / listener: simple: acknowledge-mode: manual prefetch: 1prefetch设为 1 表示每个消费者一次只预取一条消息,处理完再取下一条。这个值设大了会导致消息在消费者内存里堆积,设小了又影响吞吐。我一般从 1 开始,根据实际压测结果调整,通常 10 到 30 之间比较合理。
4.2 交换机、队列与绑定的完整声明
建一个RabbitMQConfig配置类,把三个队列、一个交换机、三个绑定关系都声明出来:
@Configuration public class RabbitMQConfig { public static final String EXCHANGE_NAME = "order.topic.exchange"; public static final String RISK_QUEUE = "queue.risk"; public static final String NOTIFY_QUEUE = "queue.notify"; public static final String ANALYTICS_QUEUE = "queue.analytics"; @Bean public TopicExchange orderTopicExchange() { return new TopicExchange(EXCHANGE_NAME, true, false); } @Bean public Queue riskQueue() { return new Queue(RISK_QUEUE, true); } @Bean public Queue notifyQueue() { return new Queue(NOTIFY_QUEUE, true); } @Bean public Queue analyticsQueue() { return new Queue(ANALYTICS_QUEUE, true); } @Bean public Binding riskBinding() { return BindingBuilder.bind(riskQueue()) .to(orderTopicExchange()) .with("order.#"); } @Bean public Binding notifyBinding() { return BindingBuilder.bind(notifyQueue()) .to(orderTopicExchange()) .with("order.create.*"); } @Bean public Binding analyticsBinding() { return BindingBuilder.bind(analyticsQueue()) .to(orderTopicExchange()) .with("order.*.success"); } }这段代码在应用启动时会自动向 RabbitMQ 声明这些资源。如果 RabbitMQ 里已经存在同名但属性不同的资源,启动会报错,这时候要么改名字,要么去管理后台删掉重建。
4.3 生产者发送消息的三种方式
生产者用RabbitTemplate发送消息,最基础的方式是调用convertAndSend:
@Autowired private RabbitTemplate rabbitTemplate; public void sendOrderCreated(OrderDTO order) { String routingKey = "order.create.success"; rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, routingKey, order); }convertAndSend会自动用配置的 MessageConverter 把对象转成 JSON 字节数组,然后发送。如果需要在消息头里带一些元数据,比如消息 ID、时间戳,可以用MessagePostProcessor:
rabbitTemplate.convertAndSend(EXCHANGE_NAME, routingKey, order, message -> { message.getMessageProperties().setMessageId(UUID.randomUUID().toString()); message.getMessageProperties().setTimestamp(new Date()); return message; });还有一种方式是直接构造Message对象发送,适合需要精细控制消息属性的场景。不过大多数业务用convertAndSend就够了。
发送时如果交换机不存在或者路由键匹配不到任何队列,消息会被丢弃(除非设置了 mandatory 标志和返回回调)。我建议在开发阶段开启发送端确认和返回回调,方便排查问题:
spring: rabbitmq: publisher-confirm-type: correlated publisher-returns: true然后在RabbitTemplate上设置ConfirmCallback和ReturnsCallback,记录日志。
4.4 三个消费者监听器的实现
消费者用@RabbitListener注解,每个队列一个方法:
@Component public class OrderMessageConsumer { @RabbitListener(queues = RabbitMQConfig.RISK_QUEUE, concurrency = "2-5") public void handleRisk(OrderDTO order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { try { // 风控逻辑:评估订单风险 System.out.println("风控模块收到订单:" + order.getOrderId()); channel.basicAck(deliveryTag, false); } catch (Exception e) { try { channel.basicNack(deliveryTag, false, true); } catch (IOException ex) { // 记录日志 } } } @RabbitListener(queues = RabbitMQConfig.NOTIFY_QUEUE, concurrency = "2-5") public void handleNotify(OrderDTO order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { try { System.out.println("通知模块收到订单:" + order.getOrderId()); channel.basicAck(deliveryTag, false); } catch (Exception e) { try { channel.basicNack(deliveryTag, false, true); } catch (IOException ex) { // 记录日志 } } } @RabbitListener(queues = RabbitMQConfig.ANALYTICS_QUEUE, concurrency = "2-5") public void handleAnalytics(OrderDTO order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { try { System.out.println("分析模块收到订单:" + order.getOrderId()); channel.basicAck(deliveryTag, false); } catch (Exception e) { try { channel.basicNack(deliveryTag, false, true); } catch (IOException ex) { // 记录日志 } } } }这里每个方法都接收Channel和deliveryTag,用于手动确认。@Header(AmqpHeaders.DELIVERY_TAG)注解用来提取消息的投递标签,这是手动确认必须的参数。
提示:如果消费者方法抛出了异常且没有手动 nack,Spring AMQP 会根据容器的
defaultRequeueRejected配置决定是否重新入队。默认是 true,会导致消息无限重试。我建议在业务代码里捕获异常并显式 nack,同时设置重试次数上限,避免死循环。
4.5 测试验证:一条消息如何命中三个队列
写一个简单的测试接口,发送一条路由键为order.create.success的消息:
@RestController public class OrderController { @Autowired private RabbitTemplate rabbitTemplate; @GetMapping("/order/create") public String createOrder() { OrderDTO order = new OrderDTO(); order.setOrderId("ORD" + System.currentTimeMillis()); order.setAmount(new BigDecimal("199.00")); rabbitTemplate.convertAndSend( RabbitMQConfig.EXCHANGE_NAME, "order.create.success", order); return "消息已发送"; } }启动应用后,访问这个接口,控制台会依次打印三个模块的日志,说明消息被成功分发到了三个队列。如果只打印了部分日志,就去 RabbitMQ 管理后台的 Exchanges 页面,查看交换机的绑定关系,确认路由键是否匹配。
再测试一下路由键order.cancel.success,预期风控和分析模块收到,通知模块收不到。再测试order.create.fail,预期只有风控模块收到。通过这几组测试,就能验证通配符匹配是否符合预期。
5. 常见问题与排查技巧实录
5.1 消息发送成功但消费者收不到
这是最常见的问题,排查思路按以下顺序进行:
第一,确认交换机是否存在。去管理后台的 Exchanges 页面看,如果交换机不存在,说明声明代码没生效,检查配置类是否被 Spring 扫描到。
第二,确认队列是否绑定了交换机。在交换机的 Bindings 区域看,如果绑定列表为空,说明绑定 Bean 没生效,或者绑定键写错了。
第三,确认路由键是否匹配。用管理后台的 "Publish message" 功能手动发一条消息,填入路由键,看能否路由到队列。如果手动发能到,代码发不能到,那就是代码里的路由键写错了。
第四,确认消费者是否在线。在 Queues 页面看队列的 Consumers 数量,如果是 0,说明消费者没启动或者监听器没注册。
我整理了一个速查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 交换机不存在 | 配置类未生效 | 检查 @Configuration 和包扫描路径 |
| 绑定关系为空 | Binding Bean 未创建 | 检查 @Bean 方法是否返回 Binding |
| 路由键不匹配 | 通配符写法错误 | 用管理后台手动测试路由 |
| 消费者数量为 0 | 监听器未注册 | 检查 @RabbitListener 和启动类注解 |
| 消息进了队列但没消费 | 消费者阻塞或异常 | 查看消费者日志和线程堆栈 |
5.2 消息重复消费与幂等处理
手动确认模式下,如果消费者处理完业务后、调用 ack 之前宕机,消息会重新入队,导致重复消费。这是 at-least-once 语义的固有特性,无法完全避免,只能通过幂等设计来兜底。
常见的幂等方案有三种:一是用数据库唯一索引,比如订单 ID 加业务类型做唯一键,重复插入会报错;二是用 Redis 记录已处理的消息 ID,处理前先查 Redis,处理完写入 Redis 并设置过期时间;三是用状态机,业务状态只能从 A 转到 B,重复消息到达时状态已经是 B,直接忽略。
我一般用 Redis 方案,简单直接。消息 ID 可以在生产者发送时设置到 MessageProperties 里,消费者取出来做判断。
5.3 消费者线程阻塞导致队列积压
如果消费者方法里有耗时操作,比如调用外部接口、大批量数据库写入,会导致消费者线程被占满,队列消息积压。排查方法是看 RabbitMQ 管理后台的队列消息数和消费者数量,如果消息数持续增长而消费者数量不变,说明消费速度跟不上生产速度。
解决办法有几个:一是增加concurrency参数,提高并发消费者数量;二是优化消费者逻辑,把耗时操作异步化,比如丢到线程池处理;三是设置prefetch参数,控制每个消费者预取的消息数,避免消息在单个消费者内存里堆积。
注意:
concurrency不是越大越好,每个消费者都会占用一个线程和一个通道,线程过多会导致上下文切换开销增大。一般建议根据 CPU 核数和 IO 等待时间来计算,IO 密集型任务可以设大一些,CPU 密集型任务设小一些。
5.4 消息序列化失败的各种报错
序列化相关的报错通常长这样:MessageConversionException: Failed to convert message或者ClassNotFoundException。原因无非是生产者和消费者的序列化方式不一致,或者实体类的包路径不同。
排查步骤:先确认双方是否都配置了Jackson2JsonMessageConverter,再确认实体类的全限定名是否一致。如果生产者用的是com.a.OrderDTO,消费者用的是com.b.OrderDTO,即使字段完全一样,JDK 序列化也会失败。换成 JSON 序列化后这个问题就不存在了,因为 JSON 只认字段名不认类名。
还有一个坑是日期类型的序列化。Jackson 默认把Date转成时间戳,消费者反序列化时如果字段类型是LocalDateTime,会报错。解决办法是在实体类上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,或者配置全局的 ObjectMapper。
5.5 交换机与队列的持久化陷阱
声明交换机时如果durable设为 false,RabbitMQ 重启后交换机会消失,所有绑定关系也没了。队列同理。生产环境一定要把durable设为 true。
但有个坑:如果 RabbitMQ 里已经存在一个同名但durable为 false 的交换机,你再声明一个durable为 true 的,会报错PRECONDITION_FAILED。这时候要么去管理后台删掉旧的,要么改个名字。我建议在开发阶段就用正确的持久化配置,避免上线时才发现问题。
另外,队列的exclusive和autoDelete属性也要注意。exclusive为 true 的队列只能被声明它的连接使用,连接断开后队列自动删除,适合临时队列。autoDelete为 true 的队列在没有消费者时会自动删除,适合临时场景。业务队列这两个属性都应该设为 false。
6. 进阶优化与生产环境建议
6.1 死信队列与延迟重试
消费者处理失败的消息如果一直重新入队,会形成死循环。更好的做法是给队列绑定一个死信交换机,处理失败的消息进入死信队列,由专门的消费者做延迟重试或者人工处理。
配置方式是在声明业务队列时指定死信交换机:
@Bean public Queue riskQueue() { return QueueBuilder.durable(RISK_QUEUE) .deadLetterExchange("dlx.exchange") .deadLetterRoutingKey("dlx.risk") .build(); }然后声明死信交换机和死信队列,绑定关系照常配置。消费者 nack 时设置requeue为 false,消息就会进入死信队列。在死信队列的消费者里,可以设置延迟时间后再重新发送到业务交换机,实现延迟重试。
6.2 消息追踪与监控
生产环境需要知道消息从哪来、到哪去、有没有被消费。RabbitMQ 自带 Firehose 追踪功能,但性能开销大,不适合长期开启。更实用的方案是在消息头里塞一个全局追踪 ID,生产者和消费者都打印这个 ID,通过日志系统串联。
另外,SpringBoot Actuator 提供了 RabbitMQ 的健康检查端点,可以集成到监控系统里。队列的消息积压数、消费者数量、消息进出速率这些指标,可以通过 RabbitMQ 的 HTTP API 采集,接入 Prometheus 和 Grafana 做可视化。
6.3 集群与高可用考量
单机 RabbitMQ 有单点故障风险,生产环境一般用集群。RabbitMQ 集群有两种模式:普通集群和镜像集群。普通集群只同步元数据,队列数据只存在一个节点上,该节点挂了队列就不可用。镜像集群会把队列数据同步到多个节点,可用性更高,但性能开销也更大。
现在更推荐的是 Quorum 队列,基于 Raft 协议实现,数据强一致,适合对可靠性要求高的场景。声明方式和普通队列类似,把QueueBuilder.durable换成QueueBuilder.durable加quorum配置即可。不过 Quorum 队列对消息大小和数量有一些限制,选型时要评估。
我在实际项目里的体会是,如果业务量不大,单机加定期备份就够了;如果业务量中等,用镜像集群;如果对数据一致性要求极高,上 Quorum 队列。没有银弹,根据实际情况选。
6.4 消息顺序性的取舍
Topic 交换机把消息分发到多个队列,每个队列内部的顺序是有保证的,但跨队列的顺序无法保证。如果业务对顺序有要求,比如订单创建必须在订单支付之前处理,那就不能让这两个消息进不同的队列,得让它们进同一个队列,由同一个消费者按顺序处理。
但这样又牺牲了并发能力。所以顺序性和并发性是一对矛盾,要根据业务场景权衡。大多数场景下,消息之间没有严格的顺序依赖,可以放心用多队列并发消费。只有少数场景,比如状态机流转,才需要保证顺序。
7. 个人实操心得与避坑清单
折腾 RabbitMQ 这些年,踩过的坑比写过的代码还多。有几个经验我觉得值得单独拎出来说。
第一,路由键的命名规范要提前定好。我见过一个项目,路由键一会儿是order.create,一会儿是orderCreate,一会儿是order_create,结果绑定关系乱七八糟,排查问题的时候想死的心都有。后来我们定了规矩:全小写,点号分隔,层级不超过四层,语义从粗到细。这个规范一立,路由匹配再也没出过问题。
第二,消费者方法一定要加 try-catch。不光是业务异常,连channel.basicAck本身都可能抛 IOException。如果异常没捕获,消息会一直重新入队,把队列撑爆。我的做法是统一封装一个确认工具类,ack 和 nack 都包一层,确保异常不会逃逸。
第三,开发阶段把 RabbitMQ 管理后台开着。很多问题在代码里看不出来,去后台一看交换机的绑定关系、队列的消息数、消费者的连接状态,一目了然。我习惯在本地开发时把管理后台的地址放在浏览器书签栏,随时点开看。
第四,消息体不要太大。RabbitMQ 对单条消息的大小没有硬性限制,但消息越大,序列化、传输、存储的开销越大。我一般建议单条消息控制在 1MB 以内,超过这个量级就考虑把大字段拆出去,消息里只传 ID,消费者按需查询。
第五,压测的时候关注 prefetch 和 concurrency 的配合。这两个参数是联动的,prefetch 控制单个消费者的预取量,concurrency 控制消费者数量。总预取量等于两者相乘,这个值决定了消息在消费者端的内存占用。压测时从保守值开始,逐步调大,观察吞吐量和延迟的变化,找到拐点。
这套 SpringBoot 加 RabbitMQ 的主题分发方案,我在几个项目里都用过,稳定性和灵活性都经得起考验。核心就是把交换机的路由规则设计好,把消费者的确认机制做扎实,剩下的就是根据业务量调参数。希望这些内容能帮你少走点弯路。