news 2026/10/16 4:42:52

Spring Boot 4.0集成RabbitMQ:注解驱动实现异步消息

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 4.0集成RabbitMQ:注解驱动实现异步消息

最近我负责的一个内部模拟项目X要从老式的 HTTP 轮询改成异步消息驱动,正好赶上 Spring Boot 4.0 的新版本落地,我顺势把 RabbitMQ 也一起接了进来。折腾了大概一周,最直观的感受是:如果还像几年前那样写一堆ConnectionFactory、Channel、消息监听容器的手动装配代码,那真是浪费了 Spring Boot 4.0 的自动装配和注解模型。标题里说的“注解魔法”一点不夸张,@EnableRabbit、@RabbitListener、@RabbitHandler组合起来之后,整个消费端的代码量少得惊人。

这篇文章就是围绕 Spring Boot 4.0 + RabbitMQ 这套组合展开的。适用读者是那些已经用过 Spring Boot,想快速把 RabbitMQ 引入项目,但对 AMQP 协议、队列交换机绑定、消息转换、手动 ACK 这些概念还比较模糊的人。我会把环境搭建、核心注解原理、发送端写法、死信和重试,还有我实测踩过的坑全部放出来,所有内容都基于我自己的真实操作经验,不是把官方文档抄一遍。

1. 为什么 Spring Boot 4.0 时代我仍然首选 RabbitMQ 这张“消息王牌”

先别急着写代码,选型这件事必须想清楚。RabbitMQ 虽然是老牌消息中间件,但我在对比过 Kafka、RocketMQ 之后,还是把它放在异步任务、订单事件、库存同步这类场景的第一位。原因很简单:这货对 AMQP 协议的实现非常完整,路由灵活,延迟低,运维找资料也容易。

1.1 从“轮询”到“事件驱动”,到底解决了什么问题

在没有消息中间件之前,我做的库存同步是定时任务每隔 30 秒扫一次数据库,把变更记录发给下游系统。这种做法有两个毛病:第一,扫描间隔导致数据延迟不可控;第二,一旦下游接口响应慢,定时任务线程池会被占满,后面全部排队。引入 RabbitMQ 之后,业务系统只要把库存变更事件扔进交换机,消费者的处理速度和业务方天然解耦,生产者不需要关心下游是否可用,下游也不用担心上游突发流量。

更重要的是,Spring Boot 4.0 对 spring-boot-starter-amqp 做了更深入的自动装配。连接工厂、管理模板、消息转换器、监听容器工厂这些组件都替你准备好了。你只需要在配置文件里写清楚host、port、username、password,再在启动类或者配置类上标注@EnableRabbit,一个可用的消息收发链路就通了。

1.2 AMQP 模型里你必须拿下的四个概念

RabbitMQ 之所以比简单的 Redis 队列难懂,是因为它多了交换机、绑定、路由键这套抽象。我用大白话解释给你:生产者不直接往队列里塞消息,而是把消息交给交换机;交换机根据路由键和绑定规则,决定消息进入哪个队列;消费者最终从队列里拿消息。

所以你必须先建立四个概念:

  • 交换机(Exchange):消息入口,负责接收生产者发来的消息。常见类型有 Direct、Topic、Fanout、Headers。
  • 队列(Queue):消息最终存储的地方,消费者从这里拉消息。
  • 绑定(Binding):把交换机和队列关联起来,并指定路由键规则。
  • 路由键(Routing Key):消息上的一个标记,交换机根据它决定投递路径。

我最早学的时候被这套东西绕晕过,后来用快递公司类比就通了:交换机是分拣中心,路由键是快递单上的地址,队列是具体的配送站点。你往分拣中心扔一件包裹,分拣中心看地址帮你扔到对应站点,消费者就是那个站点的快递员。这个类比理解透了,后面看任何配置都能对号入座。

1.3 Direct 和 Topic,我的日常选择标准

实际项目里用得最多的是DirectExchange和TopicExchange。DirectExchange 的规则是精确匹配:路由键是stock.sync,那消息的 routing key 也必须完全等于stock.sync才会进入绑定队列。适合订单支付、库存扣减这种逻辑很明确的场景。

TopicExchange 则支持通配符:*匹配一个单词,#匹配零个或多个单词。比如我监听order.#,那order.created、order.paid.event都能收到。我一般用 Topic 做领域事件分发,比如所有order.*事件都进同一个队列,再在消费端按事件类型分别处理。这样做的好处是新增一种事件类型时,不需要重新绑定队列,只要消费者里多写一个处理方法就行。

2. 依赖与容器:先把 RabbitMQ 跑起来,再说注解的事

很多人上来就写@RabbitListener,结果项目启动直接报连接拒绝,然后又一脸懵地回来查网络配置。这一步我们先解决环境和依赖,把地基打牢。

2.1 Docker 一行命令启动 RabbitMQ

本地开发我强烈推荐用 Docker,千万不要在自己电脑上手动装 Erlang 和 RabbitMQ 服务端,太折磨了。在 Docker 环境里执行:

docker run -d \ --name rabbitmq \ -p 5672:5672 \ -p 15672:15672 \ -e RABBITMQ_DEFAULT_USER=guest \ -e RABBITMQ_DEFAULT_PASS=guest \ rabbitmq:4-management

这里注意两点:5672是 AMQP 协议端口,Java 客户端连接用的是它;15672是管理控制台端口,浏览器访问http://localhost:15672可以看到图形界面。镜像名后面带management才会自带管理插件,不加的话没有 UI。启动后可以用guest/guest登录管理台,但生产环境一定不要用默认账号,这个后面坑里再说。

如果你不想用 Docker,也没关系,只要能保证本机或者测试机上有一个 RabbitMQ 服务监听 5672 端口就行。

2.2 引入 spring-boot-starter-amqp 这个核心依赖

Spring Boot 4.0 的项目里,引入 RabbitMQ 比我想象中更简单。如果你是 Maven 项目,在pom.xml里加:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>

Spring Boot 的父 POM 已经替你把版本管理好了,不需要手动写rabbitmq-version。这个 starter 会把你需要的spring-rabbit、amqp-client全拉进来。Gradle 用户对应是:

implementation 'org.springframework.boot:spring-boot-starter-amqp'

这里特别提一句:Spring Boot 4.0 里包名已经全面迁移到jakarta.*,但 Spring AMQP 的核心 API 如RabbitTemplate、MessageListenerContainer、Channel还是没有变,所以你以前写过的绝大多数 Spring AMQP 代码可以直接迁过来,只是javax开头的依赖要换成jakarta。

2.3 application.yml 里的关键配置,少一个都不行

我在application.yml里最常用的配置大概是这样的:

spring: rabbitmq: host: localhost port: 5672 username: guest password: guest virtual-host: / listener: simple: acknowledge-mode: manual prefetch: 1 retry: enabled: true max-attempts: 3 initial-interval: 2000 publisher-confirm-type: correlated publisher-returns: true template: mandatory: true

逐条解释一下。acknowledge-mode: manual意思是消费者处理完后手动给 RabbitMQ 回执,这能保证消息不会因为消费端代码抛异常被自动确认丢掉。prefetch: 1表示每个消费者同一时刻最多拿一条消息,处理完再拿下一条,避免某些消息处理慢时积压太多内存。retry.enabled控制监听器内部消费失败后重试次数。

publisher-confirm-type: correlated是发送端确认,用于生产环境必须确保消息真正进入 RabbitMQ 时配置。template.mandatory: true则是路由不到任何队列时把消息退回给生产者,配合后面要讲的ReturnsCallback可以拿到不可路由的告警。

3. 注解三件套:@EnableRabbit、@RabbitListener、@RabbitHandler 的协作关系

这里是标题里“注解魔法”的核心。如果不理解这三个注解各自身份,只照着抄,出了问题很难定位。

3.1 @EnableRabbit 是总开关,它到底激活了什么

@EnableRabbit有点像是一把钥匙。它放在配置类上,用来激活 Spring 对@RabbitListener的解析和注册。源码层面的逻辑大致是:Spring 扫描到@EnableRabbit后,会注册RabbitListenerAnnotationBeanPostProcessor和RabbitListenerEndpointRegistry这两个关键组件。前者负责扫描容器里所有 bean 的方法名上是否有@RabbitListener,后者负责把这些注解描述的方法注册成真正的消息监听端点。

如果你忘了加@EnableRabbit,注解不生效时最常见的现象是:项目启动不报错,生产者消息也发得出去,但是消费者方法永远不执行,消息全部积压在队列里 unacked。我刚开始玩的时候就被这个坑磨了一个下午,后来加了注解立刻通了。

比较干净的做法是直接加在RabbitConfig配置类上:

@Configuration @EnableRabbit public class RabbitConfig { @Bean public MessageConverter jsonMessageConverter(ObjectMapper objectMapper) { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(objectMapper); converter.setCreateMessageIds(true); return converter; } }

3.2 @RabbitListener 标注方法,监听从此零模板代码

@RabbitListener是我们最常用的注解。你可以把它标注在方法上,也可以标注在类上联合@RabbitHandler使用。方法上最简单,看一个我实际写的消费者:

@Component public class StockSyncListener { @RabbitListener(queues = "stock.sync.queue", concurrency = "3-6") public void onStockSync(StockSyncEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException { try { // 处理库存变更 updateStock(event.skuId(), event.delta()); // 手动确认 channel.basicAck(deliveryTag, false); } catch (Exception e) { channel.basicNack(deliveryTag, false, true); } } private void updateStock(String skuId, int delta) { // 实际数据库更新逻辑 } }

这里有几个细节需要讲清楚。concurrency = "3-6"意思是监听容器最低 3 个消费者、最多 6 个消费者,Spring 会根据队列积压情况动态调整。方法参数可以直接写业务对象StockSyncEvent,前提是消息转换器是 JSON 转换器,并且消息头的__TypeId__信息能被正确识别。

手动 ACK 的参数deliveryTag是通过@Header(AmqpHeaders.DELIVERY_TAG)拿到的,这是当前消息在当前 Channel 上的唯一递增序号。basicAck(deliveryTag, false)的第二个参数false表示不批量确认,只确认当前这条。

3.3 @RabbitHandler 实现同一队列多事件类型分发

如果监听器需要处理同一队列里的不同消息类型,可以用类级@RabbitListener搭配方法级@RabbitHandler。Spring 会根据消息反序列化后的实际类型,选择匹配的方法:

@Component @RabbitListener(queues = "order.event.queue") public class OrderEventHandler { @RabbitHandler public void handleCreated(OrderCreatedEvent event) { // 处理订单创建事件 } @RabbitHandler public void handlePaid(OrderPaidEvent event) { // 处理订单支付事件 } }

这种写法的好处是,一个队列对应一个业务模块,模块内不同事件类型只用不同方法处理,代码结构特别清晰。但前提是消息生产者发送时,消息头里必须包含类型信息,Jackson2JsonMessageConverter会在消息属性里写入__TypeId__,消费者端再根据这个字段找方法。如果生产端和消费端的包名不一致,反序列化会失败,后面踩坑部分会专门说。

3.4 Spring Boot 4.0 在注解驱动上的隐性增强

很多人没注意到的是,Spring Boot 4.0 里对消息监听端点的启动顺序、容器工厂配置做了更细的默认值处理。比如之前版本里如果监听容器依赖某个 Bean,必须手动配置dependsOn,现在只要 Bean 之间的依赖关系在容器中可见,监听容器会自动等待依赖就绪再启动。这意味着你在@RabbitListener方法里注入的Service如果初始化很慢,不会再出现消费者先启动然后报错的问题。

另外,Spring Boot 4.0 对虚拟线程的支持也更成熟,如果你是 JDK 21+ 环境,可以考虑在监听容器上启用虚拟线程,配置类似:

@Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( ConnectionFactory connectionFactory, MessageConverter messageConverter) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(messageConverter); factory.setTaskExecutor(Executors.newVirtualThreadPerTaskExecutor()); factory.setPrefetchCount(1); return factory; }

虚拟线程的好处是每个消息都能有一个独立的轻量线程去处理,不会被传统线程池的线程数限制死。不过这个配置要不要上,得看你本机 JDK 版本,如果还在用 JDK 17,那就老老实实用默认线程池。

4. 发送端:RabbitTemplate 的序列化、confirm 回调与消息结构设计

消费端注解省了很多事,但生产端如果图省事,随便用convertAndSend发字符串、发 JSON 字符串,后面消费端解析就会越来越乱。发送端的核心任务是设计好消息结构和确认机制。

4.1 统一 JSON 消息转换器,两端不能各玩各的

我见过最麻烦的情况是,生产端用SimpleMessageConverter发 Java 对象序列化后的字节,消费端却配了Jackson2JsonMessageConverter,结果反序列化直接报ClassCastException。两端消息转换器必须对齐。生产端可以注入RabbitTemplate,然后把自定义转换器塞进去:

@Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(messageConverter); return rabbitTemplate; }

发送消息的代码就很简单了:

rabbitTemplate.convertAndSend( "stock.exchange", "stock.sync.routing.key", new StockSyncEvent(skuId, delta) );

convertAndSend内部会自动调用转换器,把StockSyncEvent变成 Message 对象并设置contentType=application/json、__TypeId__等属性。我习惯把交换机、路由键做成常量,避免不同类里字符串拼写不一样导致消息进不了队列。

4.2 confirm 回调:生产环境到底怎么确认消息真的发出去了

RabbitMQ 是一个网络服务,生产者把消息交给它,中间可能因为网络闪断、交换机不存在、路由失败等各种原因丢掉。所以生产环境必须开启发送端确认。

配置我前面已经给了,publisher-confirm-type: correlated。然后在代码里设置回调:

rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { if (ack) { log.info("消息确认成功: {}", correlationData.getId()); } else { log.error("消息确认失败: {}, cause: {}", correlationData.getId(), cause); } });

CorrelationData是消息关联数据,发送时如果不手动设置,Spring 会生成一个 UUID。但如果你的业务需要把某条消息和数据库记录对应起来,应该在发送前就构造好CorrelationData:

CorrelationData correlationData = new CorrelationData(orderId); rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);

这样回调里的correlationData.getId()就是你的业务订单号,方便失败时做补偿。注意confirm只表示 RabbitMQ 的交换机收到了消息,不表示消息一定路由到了队列。如果路由不到,还要依赖mandatory和ReturnsCallback:

rabbitTemplate.setReturnsCallback(returned -> { log.error("消息无法路由: exchange={}, routingKey={}, replyText={}", returned.getExchange(), returned.getRoutingKey(), returned.getReplyText()); });

我自己一般会在 confirm 成功且无 return 时,才把本地消息表的状态标记为“已送达”。否则就进入定时补偿逻辑,重新发送或者告警。

4.3 消息体设计:record、版本号、TraceId

消息体本身别闹心,我推荐用 Java record 表达事件。比如:

public record StockSyncEvent(String skuId, int delta, long timestamp) { }

用 record 的好处是不用写一堆 getter/setter,序列化时字段名清晰,反序列化也稳定。但要注意:一旦消息结构上线,字段名尽量不要改,因为消费者的 JSON 解析对不上就会出问题。如果一定要扩展,宁可加新字段,也绝对不要删旧字段。

还有一个实用习惯:所有消息里都带上traceId或requestId,这样一条消息从生产端到消费端的完整链路可以串联起来。尤其当消费端处理失败需要查日志时,没有 traceId 基本靠猜。我在模拟项目X里所有事件都搞了一个统一父类或接口,包含eventId和occurredAt,后面排查问题省了很多时间。

5. 由浅入深:死信队列、重试与多消费者并发控制

跑通 Hello World 只是第一步,真正要上生产,你必须回答一个问题:消息处理失败怎么办?如果业务系统挂了十分钟,队列里的消息是堆着不管,还是立刻转到一个专门的地方,等系统恢复后再慢慢消化?这就引出了死信队列。

5.1 死信队列的配置方式,拒绝消息悄悄消失

RabbitMQ 的死信机制很简单:给一个队列配置x-dead-letter-exchange和x-dead-letter-routing-key,当队列中的消息被拒绝、消费超时或者队列长度超过限制时,消息会转发到指定的死信交换机,再路由到死信队列。

我在 Spring 配置里通常是声明一套完整的三件套:

@Bean public DirectExchange stockExchange() { return new DirectExchange("stock.exchange"); } @Bean public Queue stockQueue() { return QueueBuilder.durable("stock.sync.queue") .deadLetterExchange("stock.dlx") .deadLetterRoutingKey("stock.sync.dlq.routing.key") .build(); } @Bean public DirectExchange stockDlx() { return new DirectExchange("stock.dlx"); } @Bean public Queue stockDlq() { return QueueBuilder.durable("stock.sync.dlq").build(); } @Bean public Binding stockBinding() { return BindingBuilder.bind(stockQueue()).to(stockExchange()).with("stock.sync.routing.key"); } @Bean public Binding stockDlqBinding() { return BindingBuilder.bind(stockDlq()).to(stockDlx()).with("stock.sync.dlq.routing.key"); }

这套 Bean 声明之后,Spring Boot 启动时会自动把交换机、队列、绑定执行到 RabbitMQ 服务端。消费者方法不需要变化,如果代码抛异常并且做了basicNack(deliveryTag, false, false),消息就会被转到死信队列。后面我单独写一个死信消费者,做人工补偿或者重新入队。注意basicNack的第三个参数如果是true,表示重新放入原队列,会形成无限循环,如果你不是故意做重试,一定不要写true。

5.2 消费重试:本地重试和死信配合,怎么选

Spring AMQP 提供了重试拦截器。配置里开启spring.rabbitmq.listener.simple.retry.enabled=true后,消费方法抛异常不会立刻 NACK,而是 Sleeper 等待后重试,达到max-attempts后才会真正把消息拒绝掉。这适合处理下游临时抖动,比如数据库连接池短暂满了、HTTP 调用刚好超时。

但本地重试有个风险:重试期间消息在消费者处理流程里,队列中的消息并没有被重新投递。如果你的消费者线程很少,一条消息重试 3 次,每次间隔 5 秒,你一个消费者可能因为这一条消息卡住 15 秒,后面 prefetch 的消息全部等在那里。所以我一般把prefetch设置为 1,让重试影响范围最小化。

更稳妥的做法是:本地重试只设置 1 到 2 次,失败之后直接basicNack进死信队列,由死信消费者决定什么时候、以什么频率重新处理。这样主队列不会被一个坏消息堵塞,补偿节奏也更可控。

5.3 并发消费者数量:不是越大越好,线程数和连接数都要盯

@RabbitListener(queues = "stock.sync.queue", concurrency = "3-6")里的并发数字,Spring 会为每个并发消费者创建一条独立的消费通道,但底层连接是共享的,由CachingConnectionFactory统一管理。并发越大,同一时刻从队列拉出来的消息就越多。你以为并发越大处理越快,但实际上如果你的下游是数据库,并发飙到 20 可能直接把数据库连接池打满。

我的经验公式是:并发消费者数不要超过下游资源池的 1/3。比如数据库连接池最大 20,那我监听并发最多配置到 6,否则一条慢 SQL 会导致整个系统雪崩。另外,如果你同时有多个@RabbitListener,每个监听器默认会共用一个连接工厂创建的连接。理论上一个 Connection 可以开多个 Channel,但如果监听器极多或者消息量极大,建议单独建一个ConnectionFactory给关键业务的监听器,避免互相影响。

5.4 幂等消费:RabbitMQ 的语义是至少一次,不是刚好一次

这一点必须单独说。RabbitMQ 消费者收到消息后,如果处理到一半进程崩溃,连接断开,消息会被重新投递。所以你的消费逻辑必须是幂等的。比如库存同步,不能简单“加上 delta”,而要去重后累加。我常用的做法是:在数据库中建一张消息去重表,主键是eventId;消费逻辑先insert ignore这个事件 ID,插入成功才处理业务,插入失败说明已经处理过,直接 ACK 跳过。

boolean firstProcess = deduplicateService.tryInsert(event.eventId()); if (!firstProcess) { channel.basicAck(deliveryTag, false); return; } try { doBusiness(event); channel.basicAck(deliveryTag, false); } catch (Exception e) { channel.basicNack(deliveryTag, false, false); }

这里tryInsert返回 false 表示重复消息,直接 ACK 是最佳处理,不然它会一直进入死信队列。幂等表记得清理历史数据,保留最近 7 天就够,不然数据量涨得很快。

6. 实测中踩过的几个坑:注解不生效、序列化不一致、连接风暴

最后这部分是我最想分享的,因为代码样例谁都能抄,但这些坑不实际踩一遍真的不知道。

6.1 @RabbitListener 一直不执行,先查 @EnableRabbit 和监听容器工厂

前面提过,忘加@EnableRabbit是注解不生效的最常见原因。但还有另一种情况:你加了@EnableRabbit,消费者方法也确实注册了,可消息就是不进消费方法。这时要看是不是自定义了SimpleRabbitListenerContainerFactory,并且没有设置messageConverter。如果你创建了自定义工厂,Spring Boot 原来的自动配置工厂就不会被使用,消费者反序列化失败后可能直接报错并重试。检查顺序应该是:

  1. 项目中有没有@EnableRabbit。
  2. spring.rabbitmq.listener.simple.*配置是否生效。
  3. 有没有自定义RabbitListenerContainerFactory且配置完整。
  4. 管理控制台里看消息是ready还是unacked。如果一直是ready但消费者不掉,大概率是监听器没注册成功或 preflight 失败。

6.2 生产端用 Java 序列化,消费端用 JSON,两个经典报错

第一个报错是ClassNotFoundException或NoClassDefFoundError,常见于生产端没改MessageConverter,默认用了SimpleMessageConverter(JDK 序列化)。消费者却配了Jackson2JsonMessageConverter。解决方案是排除所有自动配置的MessageConverter,全局指定同一个。

第二个报错更隐蔽:生产端和消费端虽然都是 JSON,但包子路径不同。比如生产端类是com.company.order.event.OrderCreatedEvent,消费端同名类在com.company.order.listener.event.OrderCreatedEvent,Jackson2JsonMessageConverter根据消息头里的__TypeId__找类时找不到。解决办法有两个:一是严格统一依赖中的类路径,二是配置AllowedList或者自定义TypeIdMapping。我在项目里为了省事,直接把生产者和消费者放到同一个依赖模块,确保类型类完全一致。

6.3 手动 ACK 忘写 basicAck 导致 unacked 堆积

如果你把acknowledge-mode设成了manual,那你的消费方法必须自己调用channel.basicAck或basicNack。我见过有人把 ACK 模式设成 manual,但方法体里没写任何 ACK 代码,结果每条消息都被消费者拉出来,标记为unacked,既不处理也不确认,RabbitMQ 默认情况下 unacked 消息不会被重新投递给其他消费者。看起来像队列积压,其实堆积在消费者内存里。

解决这事只能靠规范:只要 manual 模式,统一封装一个消费模板方法,try 里处理业务并 ACK,catch 里判断是否重试或 NACK。我后来把所有消费者都改成同一个抽象父类,把 ACK 逻辑收敛起来,减少漏写的概率。

6.4 连接风暴:CachingConnectionFactory 的缓存参数

消息连接数突然暴涨也是一个容易踩的坑。Spring 默认CachingConnectionFactory会复用连接,但 Channel 有缓存上限,默认大小是 25。如果你在高并发下创建了很多RabbitTemplate或监听容器,超过缓存上限就会反复创建新连接。再加上你配置了多个@RabbitListener且并发很高,网络层面的连接数可能快速膨胀。

我的做法是显式调大channelCacheSize,并关掉不必要的事务化:

@Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory factory = new CachingConnectionFactory(); factory.setHost("localhost"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setChannelCacheSize(50); factory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); return factory; }

但这也不是越大越好。Channel 不是线程完全无锁的,缓存太多会浪费内存。一般 50 足够支撑几百并发发送,除非你有极端流量,否则别往几千调。

最后再说一点我个人的使用习惯:所有关键的交换机、队列、路由键常量,我都会集中放到一个RabbitConstants类里,消费者和生产端都引用同一份,杜绝字符串拼写错误。消息结构统一用 record,事件里带eventId和occurredAt。每次改动消息结构之前,先想清楚线上有没有未消费完的旧消息,如果有,新旧结构必须兼容,否则消费者爬起来就是一堆反序列化异常。Spring Boot 4.0 和 RabbitMQ 这套组合,用好了确实是“注解魔法”,但魔法背后还是 AMQP 那套稳定的投递模型,理解模型比背注解重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 4:42:33

L-BFGS算法精讲:高维优化中的省内存拟牛顿法

做优化的人&#xff0c;大概都经历过这种尴尬&#xff1a;目标函数公式不复杂&#xff0c;梯度也能算&#xff0c;可变量一上万&#xff0c;常规方法就顶不住了。这个时候&#xff0c;L-BFGS几乎是最顺手的工具。它的全称是Limited-memory BFGS&#xff0c;翻译过来就是“省内存…

作者头像 李华
网站建设 2026/10/16 4:42:26

大厂RAG为何不用纯向量检索?揭秘混合检索落地实践

我做过不少 RAG 项目&#xff0c;也看过很多团队从“向量检索一把梭”到“老老实实上混合检索”的全过程。标题这句话说得挺直接——大厂 RAG 从不用纯向量检索&#xff0c;听上去有点绝对&#xff0c;但对比一下生产环境和 Demo 的差距&#xff0c;你会发现这几乎是必然的选择…

作者头像 李华
网站建设 2026/10/16 4:41:54

群晖DDNS解析配置全攻略:从公网IP到Cloudflare脚本实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 4:41:49

AirLLM单卡跑70B大模型:分层卸载与显存优化实战指南

别说70B&#xff0c;哪怕13B的模型想把整套权重塞进一块十几GB显存的显卡里都费劲。但有一类方案专门干这个事——AirLLM&#xff0c;一个主打"单卡跑大模型"的开源推理工具&#xff0c;通过把模型逐层拆开、按需搬运&#xff0c;让普通显卡也能运行数百亿参数级别的…

作者头像 李华
网站建设 2026/10/16 4:41:49

NSGA-III多目标优化算法MATLAB实现:参考点机制与代码解析

简介&#xff1a;第三代非支配排序遗传算法&#xff08;NSGA-III&#xff09;的MATLAB完整实现代码&#xff0c;源自开源项目Yarpiz&#xff0c;主要面向需要处理高维、多目标复杂优化问题的科研人员、工程技术人员及相关专业学生。该算法借助非支配排序、拥挤距离与参考点机制…

作者头像 李华