ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JmsTemplate集成ActiveMQ实战:配置、事务与性能调优

JmsTemplate集成ActiveMQ实战:配置、事务与性能调优

消息中间件这块,真正在生产环境里跑过几年的人都有一个共识:JMS 这套 API 本身不难,难的是把它用对、用稳。JmsTemplate是 Spring 对 JMS 规范的一层封装,用起来确实省事,几行代码就能把消息发出去、收进来,但恰恰是这种"太省事"的封装,让很多人在项目上线之后才发现一堆问题——消息丢了、重复消费了、事务边界搞不清楚、连接池配错了导致吞吐上不去。这篇内容就围绕JmsTemplate集成 ActiveMQ 这条主线,把配置、发送、接收、事务、异常处理、性能调优这些环节拆开讲透,适合已经上手过 Spring 但还没在消息队列上踩过太多坑的开发者,也适合正在做技术选型、想搞清楚 JMS 这套东西到底该怎么落地的朋友。

我个人的判断是:如果你所在的系统需要的是标准的点对点或发布订阅模型,且团队对 JMS 规范比较熟悉,那JmsTemplate加 ActiveMQ 的组合依然是相当稳妥的选择;但如果你追求的是极高的吞吐或者复杂的路由能力,那可能得另做打算。下面这些内容,都是围绕"怎么把这套组合用扎实"来展开的。

1. 先把 JmsTemplate 的定位和边界搞清楚

很多人一上来就急着写配置、发消息,结果遇到问题的时候连该往哪个方向排查都不知道。所以在动手之前,得先弄明白JmsTemplate到底是个什么东西,它替你做了什么,又没替你做什么。

1.1 JmsTemplate 在 Spring 体系里扮演的角色

JmsTemplate是 Spring Framework 提供的一个帮助类,它的核心职责是把 JMS 规范里那些繁琐的样板代码——创建连接、创建会话、创建生产者/消费者、发送/接收、关闭资源——全部封装起来。你只需要给它一个ConnectionFactory,再调一下send()或receive(),剩下的它帮你处理。

但这里有个关键点必须说清楚:JmsTemplate是同步的。它不像一些现代消息客户端那样内置异步回调或者响应式流,send()调用返回的时候,消息要么已经交给了 broker,要么抛了异常。receive()更是阻塞式的,没消息就一直等(或者等到超时)。这个特性决定了它的适用场景——适合那些对实时性要求不是极端苛刻、但要求逻辑清晰的业务,比如订单状态变更通知、异步任务派发、系统间解耦。

我在实际项目里见过有人拿JmsTemplate去做高并发的实时推送,结果线程池被打满,这就是没搞清楚它的同步本质。如果你的场景需要极高的并发吞吐,应该考虑用消息监听容器(DefaultMessageListenerContainer)配合多消费者,而不是在主线程里死等receive()。

1.2 它替你做的和没替你做的

JmsTemplate帮你做的主要是资源管理:每次操作自动获取连接、会话,用完自动关闭(或者归还到池里)。它还帮你做了消息转换——你可以直接发一个 Java 对象,只要配了MessageConverter,它自动序列化成TextMessage或BytesMessage。

但它没有替你做的事情同样重要:

  • 它不保证消息一定送达。默认情况下JmsTemplate用的是AUTO_ACKNOWLEDGE模式,消息发出去之后如果 broker 那边出了问题,你这边是感知不到的。要保证可靠性,得配合事务或者持久化配置。
  • 它不处理重复消费。消息中间件普遍是"至少一次"投递语义,重复消息是常态,幂等逻辑得你自己在业务层做。
  • 它不管连接池。JmsTemplate每次操作都会向ConnectionFactory要连接,如果你用的是 ActiveMQ 原生的ActiveMQConnectionFactory而不套连接池,那每次发送都新建 TCP 连接,性能会非常难看。

提示:把JmsTemplate理解成一个"语法糖"而不是"可靠性保障",这个认知差是很多线上事故的根源。

1.3 什么场景适合用它

结合我自己的经验,下面这几类场景用JmsTemplate是比较顺手的:

场景类型是否适合原因
异步任务派发(发完就不管结果)适合逻辑简单,同步发送足够
系统间解耦的通知适合消息量可控,可靠性靠持久化兜底
请求-响应模式(等对方回消息)谨慎需要额外处理关联ID和超时
高并发实时推送不适合同步模型会成为瓶颈
大批量消息导入适合但需调优要开事务批处理,否则性能差

这张表不是绝对的,但它能帮你快速判断方向。我见过最典型的误用,是把JmsTemplate当成一个"万能发消息工具",什么场景都往里塞,最后发现要么性能不行,要么可靠性不够。

2. 环境搭建:连接工厂和连接池是性能的分水岭

配置这块,表面上看就是几个 Bean 的事,但真正决定系统能不能扛住压力的,往往就是连接工厂这一层的选择。我见过太多项目,代码写得没问题,就是连接池没配,QPS 一上去就各种超时。

2.1 ActiveMQConnectionFactory 的基础配置

最朴素的配置是直接 new 一个ActiveMQConnectionFactory,指定 broker 的地址。Spring Boot 环境下,如果你引入了spring-boot-starter-activemq,它会自动帮你配一个,你只需要在配置文件里写上地址就行。

但我要强调的是,自动配置只适合开发和测试。生产环境你必须显式地控制连接工厂的参数,尤其是下面这几个:

  • brokerURL:broker 地址,多个地址用逗号分隔可以做故障转移。
  • userName/password:认证信息,别硬编码在代码里,走配置中心或环境变量。
  • useAsyncSend:是否异步发送。开了之后send()立即返回,吞吐上去了,但可靠性下降,需要配合其他机制。
  • alwaysSyncSend:强制同步发送,和上面相反,用于对可靠性要求极高的场景。

这几个参数之间存在取舍关系,不是全开就好。useAsyncSend=true能显著提升发送吞吐,因为发送线程不用等 broker 的确认;但代价是如果 broker 挂了,你这边可能已经"以为发出去了"。我的建议是:普通通知类消息可以开异步发送,涉及资金、订单状态这类关键消息,老老实实同步发送。

2.2 为什么一定要套连接池

这是我最想强调的一点。ActiveMQConnectionFactory本身不是连接池,它每次createConnection()都会建立一个新的 TCP 连接。而JmsTemplate每次操作都会调createConnection(),如果你不套池,等于每发一条消息就建一次连接、断一次连接,这个开销在低并发下看不出来,一旦并发上来,TCP 握手和认证的时间会直接把你的吞吐拖垮。

正确的做法是用PooledConnectionFactory(ActiveMQ 自带)或者CachingConnectionFactory(Spring 提供)把原生工厂包一层。两者的区别我整理了一下:

对比项PooledConnectionFactoryCachingConnectionFactory
提供方ActiveMQSpring
缓存对象Connection、Session、ProducerConnection、Session、Producer
与 JmsTemplate 配合良好更贴合 Spring 体系
配置复杂度中等简单
推荐场景纯 ActiveMQ 项目Spring 全家桶项目

我个人的偏好是 Spring 项目用CachingConnectionFactory,因为它和JmsTemplate的配合更自然,缓存策略也更透明。配置的时候注意sessionCacheSize这个参数,默认是 1,意味着每个连接只缓存一个会话,高并发下会成为瓶颈,一般建议调到 10 到 20 之间,具体看你的并发量。

@Bean public CachingConnectionFactory cachingConnectionFactory() { ActiveMQConnectionFactory activeMQFactory = new ActiveMQConnectionFactory(); activeMQFactory.setBrokerURL("tcp://localhost:61616"); activeMQFactory.setUserName("admin"); activeMQFactory.setPassword("admin"); CachingConnectionFactory cachingFactory = new CachingConnectionFactory(); cachingFactory.setTargetConnectionFactory(activeMQFactory); cachingFactory.setSessionCacheSize(20); return cachingFactory; }

这段配置里,sessionCacheSize=20是我根据中等并发场景估的。如果你的系统峰值并发在几十个线程级别,20 够用;如果更高,可以往上调,但别无限调大,会话缓存太多反而会占用 broker 端资源。

2.3 JmsTemplate 本身的几个关键开关

连接工厂配好之后,JmsTemplate还有几个参数值得单独拎出来说:

  • setDefaultDestination:设置默认目的地,这样send()的时候不用每次传 destination。
  • setMessageConverter:消息转换器,发对象的时候必须配,否则只能发字符串。
  • setExplicitQosEnabled:是否启用显式的 QoS 设置(比如设置消息的优先级、过期时间、持久化模式)。默认是 false,意味着你调setDeliveryPersistent之类的都不生效,这个坑很多人踩过。
  • setSessionTransacted:是否启用会话事务,这个后面单独讲。

setExplicitQosEnabled(true)这个开关特别容易被忽略。我见过一个项目,代码里明明写了jmsTemplate.setDeliveryPersistent(true),结果消息重启后全丢了,排查半天才发现是没开这个开关,持久化设置根本没生效。这种问题不看源码很难发现,因为代码不报错,只是行为不符合预期。

3. 消息发送:从简单调用到可靠投递

发送消息看起来是最简单的部分,jmsTemplate.send()一行就完事。但"发出去"和"可靠地发出去"是两回事,中间隔着事务、持久化、异常处理好几道坎。

3.1 三种发送方式的取舍

JmsTemplate提供了几种发送方法,用哪种取决于你的需求:

  1. send(Destination, MessageCreator):最底层的方式,你自己构造Message对象,控制力最强,但代码最啰嗦。
  2. convertAndSend(Destination, Object):配合MessageConverter,直接发 Java 对象,最常用。
  3. sendAndReceive(...):发送并等待回复,用于请求-响应模式,慎用,容易阻塞。

日常开发里convertAndSend用得最多,因为业务代码里通常都是发一个 DTO 对象。但要注意,MessageConverter的选择会影响消息在 broker 上的存储格式。默认的SimpleMessageConverter对String、byte[]、Serializable对象处理得不错,但对复杂对象会走 Java 序列化,跨语言场景就不合适了。如果接收方不一定是 Java 系统,建议换成MappingJackson2MessageConverter,用 JSON 格式传输,通用性更好。

@Bean public MessageConverter jacksonMessageConverter() { MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); converter.setTargetType(MessageType.TEXT); converter.setTypeIdPropertyName("_type"); return converter; }

这里setTypeIdPropertyName("_type")的作用是在消息头里带上类型信息,接收方反序列化的时候能知道该转成什么类。这个细节在跨系统通信时特别重要,否则接收方拿到 JSON 不知道对应哪个 DTO。

3.2 持久化:消息不丢的第一道防线

ActiveMQ 默认对非持久化消息走内存,持久化消息走磁盘(KahaDB 或 JDBC 存储)。要让消息在 broker 重启后还在,必须满足两个条件:消息本身是持久化的,且 broker 配置了持久化存储。

在JmsTemplate这边,设置持久化的方式是:

jmsTemplate.setExplicitQosEnabled(true); jmsTemplate.setDeliveryPersistent(true);

再次强调setExplicitQosEnabled(true)不能漏。这两行代码的意思是"我明确要求这条消息是持久化的",broker 收到后会把它写到磁盘再确认。

但持久化不是免费的,写磁盘会显著降低吞吐。所以我的经验是:按消息重要性分级。关键业务消息(订单、支付)必须持久化;日志、埋点这类丢了也无所谓的消息,可以走非持久化,换取更高的吞吐。这种分级策略在实际项目里非常实用,比一刀切地全开持久化要合理得多。

3.3 事务发送:要么全成功要么全失败

当你需要一次发送多条消息,且要求它们要么都成功要么都失败时,就得用事务。JmsTemplate的事务有两种开启方式:

  • 设置setSessionTransacted(true),然后在JmsTemplate的回调里执行多次发送,最后统一提交。
  • 用 Spring 的@Transactional注解,配合JmsTransactionManager。

第一种方式更轻量,适合单纯的消息发送场景:

jmsTemplate.setSessionTransacted(true); jmsTemplate.execute(session -> { // 这里可以多次发送,最后统一提交 return null; }, true);

第二种方式适合消息发送和数据库操作需要保持一致的场景,但要注意,JMS 事务和数据库事务默认是两个独立的事务,不是分布式事务。也就是说,如果数据库提交成功但 JMS 提交失败,数据就不一致了。要真正解决这个问题,得引入分布式事务方案或者用本地消息表这种补偿机制。这一点很多人有误解,以为加了@Transactional就万事大吉,实际上跨资源的事务一致性远没那么简单。

注意:setSessionTransacted(true)和AUTO_ACKNOWLEDGE是互斥的,开了事务之后确认模式由事务提交来控制,不要再手动设确认模式。

3.4 发送失败的异常处理

JmsTemplate发送失败会抛JmsException,这是受检异常体系下 Spring 转换后的运行时异常。常见的失败原因有几类:

  • 连接不上 broker:JmsException包装了底层的连接异常。
  • 目的地不存在:如果 broker 没配置自动创建目的地,会报错。
  • 消息太大:超过 broker 配置的最大消息大小。
  • 认证失败:用户名密码不对。

处理策略上,我一般分两种:可重试的和不可重试的。连接类异常通常可重试,用 Spring Retry 包一层,配个指数退避;消息格式或大小问题不可重试,直接记录日志告警,别浪费资源重试。

@Retryable(value = JmsException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2)) public void sendOrderMessage(OrderDTO order) { jmsTemplate.convertAndSend("order.queue", order); }

这段代码的意思是发送失败后重试 3 次,间隔分别是 1 秒、2 秒、4 秒。指数退避的好处是给 broker 恢复的时间,避免雪崩式重试把 broker 压垮。

4. 消息接收:同步、异步与监听容器

接收这块是JmsTemplate相对薄弱的地方。它提供的receive()是同步阻塞的,实际项目里几乎不会直接用它,更多是用消息监听容器。但理解同步接收有助于搞清楚底层机制。

4.1 同步接收为什么不适合生产

jmsTemplate.receive()会阻塞当前线程直到有消息到达或者超时。如果你在 Web 请求线程里调它,用户请求就会被挂住,这显然不行。就算你放到独立线程里轮询,也有几个问题:

  • 线程利用率低,大部分时间在等待。
  • 消息量大时,单线程处理不过来。
  • 异常处理麻烦,一个线程挂了消息就积压。

所以同步接收基本只用于测试或者极特殊的场景。生产环境请直接用DefaultMessageListenerContainer。

4.2 DefaultMessageListenerContainer 的正确配置

DefaultMessageListenerContainer(简称 DMLC)是 Spring 提供的消息监听容器,它管理一组消费者线程,持续从目的地拉消息并回调你的监听器。核心配置项有这么几个:

配置项作用建议值
concurrentConsumers初始消费者线程数根据消息量,一般 3-10
maxConcurrentConsumers最大消费者线程数初始值的 2-3 倍
sessionTransacted是否启用事务性会话需要可靠消费时开
sessionAcknowledgeMode确认模式事务开启时用 SESSION_TRANSACTED
receiveTimeout接收超时1000-5000 毫秒
@Bean public DefaultMessageListenerContainer messageListenerContainer( ConnectionFactory connectionFactory, MessageListener orderListener) { DefaultMessageListenerContainer container = new DefaultMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setDestinationName("order.queue"); container.setMessageListener(orderListener); container.setConcurrentConsumers(5); container.setMaxConcurrentConsumers(15); container.setSessionTransacted(true); return container; }

这里concurrentConsumers=5到maxConcurrentConsumers=15的弹性设计,是为了应对消息量的波动。消息少的时候 5 个线程够用,积压时自动扩到 15 个。但要注意,线程数不是越多越好,每个消费者都会占用 broker 端的资源,盲目调大反而会降低整体效率。

4.3 消息确认模式的选择逻辑

确认模式决定了消息什么时候从 broker 上删除。ActiveMQ 支持几种模式:

  • AUTO_ACKNOWLEDGE:消息交给监听器后自动确认,如果监听器处理失败,消息就丢了。
  • CLIENT_ACKNOWLEDGE:客户端手动调acknowledge()确认,控制力强但代码复杂。
  • DUPS_OK_ACKNOWLEDGE:延迟确认,可能重复消费,但性能好。
  • SESSION_TRANSACTED:事务模式,监听器正常返回才提交,抛异常则回滚重投。

我的建议很明确:需要可靠消费就用SESSION_TRANSACTED。它把"处理消息"和"确认消息"绑在一个事务里,处理失败自动回滚,消息会重新投递。代价是可能重复消费,所以业务层必须做幂等。

AUTO_ACKNOWLEDGE看似省事,但它是"收到即确认",监听器里如果抛异常,消息已经确认掉了,等于丢了。这个坑我在早期项目里踩过,当时以为自动确认就是"处理完自动确认",实际上不是,它是"交付即确认"。

4.4 幂等处理是绕不过去的坎

既然用了事务性会话,重复消费就不可避免。幂等的实现方式有几种,我按推荐程度排个序:

  1. 业务唯一键 + 数据库唯一约束:最可靠,插入时如果冲突就说明重复了,直接忽略。
  2. Redis 记录已处理的消息ID:性能好,但要考虑 Redis 的持久化和过期策略。
  3. 状态机判断:根据业务状态判断是否已处理,适合有明确状态流转的场景。
public void onMessage(Message message) { String msgId = message.getJMSMessageID(); // 用 Redis 做去重,SETNX 保证原子性 Boolean firstTime = redisTemplate.opsForValue() .setIfAbsent("msg:" + msgId, "1", Duration.ofHours(24)); if (Boolean.FALSE.equals(firstTime)) { return; // 已处理过,直接跳过 } // 正常业务处理 processOrder(message); }

这段代码用setIfAbsent实现原子性的去重判断,过期时间设 24 小时是为了防止 Redis 无限增长。24 小时这个值要根据业务的消息重投窗口来定,如果 broker 的重投间隔可能超过 24 小时,那过期时间就得相应拉长。

5. 踩坑实录:几个真实排查过的典型问题

光讲配置和原理不够,实际项目里出问题的时候,往往是一些很隐蔽的细节。这一节我挑几个自己真实排查过的问题,把排查链路完整还原出来,方便你遇到类似情况时对照。

5.1 消息发了但消费端收不到:从连接池到目的地的排查链

有一次线上反馈,某个队列的消息发送端日志显示发送成功,但消费端就是没动静。排查过程是这样的:

第一步,先确认消息到底有没有到 broker。登录 ActiveMQ 的管理控制台,看目标队列的 Enqueue 计数。结果发现 Enqueue 是 0,说明消息根本没进队列,问题在发送端。

第二步,检查发送端的JmsTemplate配置。发现setExplicitQosEnabled没开,但这只影响持久化,不影响消息是否入队。继续看目的地配置,发现代码里用的是convertAndSend("order.queue", order),而消费端监听的是order.queue,名字看起来一致。

第三步,怀疑是连接池的问题。检查CachingConnectionFactory的配置,发现sessionCacheSize设的是 1,且并发很高。但这会导致性能问题,不会导致消息丢失。

第四步,抓包看实际发出的消息。发现发送端连的 broker 地址和消费端不是同一个——发送端配置里写的是测试环境的地址,生产环境部署时配置没覆盖。这就是根因。

这个案例的教训是:排查消息问题,第一步永远是确认消息有没有到 broker,管理控制台的计数是最直接的证据。别一上来就怀疑代码逻辑,很多时候是配置或环境的问题。

5.2 消费端反复重投同一条消息

另一个典型问题:某条消息被反复投递,消费端日志里同一条消息处理了十几次。这种问题通常是消费逻辑抛异常导致的回滚重投。

排查思路:先看消费端日志有没有异常堆栈。如果有,定位到具体是哪行代码抛的。常见原因包括:反序列化失败(消息格式和预期不符)、数据库连接超时、空指针。

如果日志里没有明显异常,那可能是事务提交阶段出的问题。比如监听器方法正常返回了,但事务提交时数据库连接断了,导致回滚,消息重新投递。

还有一种隐蔽情况:消息本身有问题,比如某个字段是 null,导致业务逻辑每次都抛异常,于是无限重投。这时候需要配置死信队列(DLQ),让重试多次仍失败的消息进入死信,避免无限循环。

<!-- ActiveMQ 的死信策略配置 --> <policyEntry queue=">" > <deadLetterStrategy> <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true" /> </deadLetterStrategy> </policyEntry>

这段配置的意思是每个队列都有自己的死信队列,前缀是DLQ.。这样出问题的消息会被转移到死信队列,人工介入处理,不会一直卡在重投循环里。

5.3 高并发下发送超时:连接池参数的血泪教训

这个坑我印象最深。一个批量导入功能,单线程测试没问题,一上并发就大量超时。排查下来是连接池配置不合理:

  • sessionCacheSize太小,高并发下频繁创建会话。
  • 没有设置sendTimeout,默认是无限等待,broker 一慢就全卡住。
  • useAsyncSend没开,每条消息都同步等确认。

调整方案是:sessionCacheSize调到 20,sendTimeout设为 3000 毫秒,对非关键消息开启useAsyncSend。调整后吞吐提升了大概三倍,超时基本消失。

这里有个经验:sendTimeout一定要设。不设的话,broker 响应慢的时候,发送线程会一直挂着,线程池很快就被耗尽,整个系统雪崩。设了超时之后,至少能快速失败,把资源释放出来。

6. 性能调优与生产环境的一些经验

配置跑通只是起点,真正让这套东西在生产环境稳定运行,还得在调优和监控上下功夫。这一节聊聊我积累的一些实操经验。

6.1 批量发送与事务批处理

如果业务场景需要发送大量消息(比如批量导入、批量通知),逐条convertAndSend的效率很低,因为每条都是一次网络往返。这时候可以用事务批处理:

jmsTemplate.setSessionTransacted(true); jmsTemplate.execute(session -> { for (OrderDTO order : orderList) { jmsTemplate.convertAndSend("order.queue", order); } return null; }, true);

在事务里连续发送,最后统一提交,broker 端可以批量处理,吞吐能提升一个数量级。但要注意事务的大小,一次事务里塞太多消息会占用大量内存,一般建议每批 100 到 500 条提交一次。

6.2 消息大小与序列化格式的权衡

消息体越大,传输和存储的开销越大。我见过有人把整个大对象图序列化后塞进消息,单条消息几 MB,结果 broker 的存储很快被撑爆。合理的做法是:

  • 消息体只放必要字段,大对象存到共享存储(如对象存储),消息里放引用。
  • 序列化格式优先选 JSON 而不是 Java 序列化,前者体积小、跨语言、可读性好。
  • 如果确实需要传大文件,考虑用BytesMessage分块传输,或者干脆走文件传输通道,别用消息队列。

ActiveMQ 默认的最大消息大小是 100MB 左右(可配置),但实际使用中,超过 1MB 的消息就要谨慎了,因为大消息会阻塞 broker 的处理线程。

6.3 监控指标该看哪些

生产环境跑起来之后,光靠日志不够,得有监控。我一般关注这几个指标:

指标含义告警阈值建议
队列积压数待消费消息数量持续增长超过 1000 告警
消费者数量当前活跃消费者低于预期值告警
消息入队/出队速率吞吐量出队速率持续低于入队速率告警
死信队列消息数处理失败的消息大于 0 就关注
连接数当前连接池使用情况接近上限告警

这些指标 ActiveMQ 的管理控制台都能看到,也可以通过 JMX 暴露出来接到监控系统。队列积压是最直观的健康指标,积压持续增长说明消费能力跟不上生产速度,要么加消费者,要么查消费逻辑是不是变慢了。

6.4 一个容易被忽略的点:broker 端的持久化存储

最后提一个经常被忽略的方面:broker 端的存储配置。ActiveMQ 默认用 KahaDB,它的性能受磁盘 IO 影响很大。如果 broker 部署在普通机械盘上,持久化消息的写入会成为瓶颈。有条件的话,broker 的数据目录应该放在 SSD 上,并且定期清理已消费的消息日志(KahaDB 有自动清理机制,但要确认配置生效)。

另外,如果消息量特别大,可以考虑用 LevelDB 或者 JDBC 存储,但各有取舍,KahaDB 在大多数场景下是够用的,别为了追求极致性能盲目换存储,换之前一定要压测验证。

我在实际项目里的体会是,消息中间件这套东西,配置和代码只是基础,真正决定稳定性的往往是对边界情况的理解——消息会不会丢、会不会重复、积压了怎么办、broker 挂了怎么恢复。把这些想清楚了,JmsTemplate加 ActiveMQ 这套组合用起来其实相当省心。最后再分享一个小技巧:上线前一定要做一次 broker 重启演练,看看消息能不能正常恢复、消费端能不能自动重连,这个演练能暴露很多平时发现不了的问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进