JMS与ActiveMQ实战笔记:从消息模型到可靠消息机制
我第一次正儿八经看 JMS 和 ActiveMQ并不是因为要面试背八股而是项目里两个服务之间的同步调用把数据库连接池打爆了。上游接口一个超时下游跟着雪崩运维的告警群半夜响成一片。翻来覆去折腾了几天之后同事丢给我一句话“用消息中间件把调用拆开吧先看看 JMS 和 ActiveMQ。”当时我连 JMS 和 MQ 的关系都说不清楚硬着头皮把那套消息模型啃了一遍才有了这篇“JMS、ActiveMQ 学习一则”。这篇笔记适合两类人一是刚开始接触消息中间件、被一堆 JMS API 术语搞晕的开发者二是能把 Demo 跑通但不清楚 Queue、Topic、ACK、持久化到底意味着什么的人。我会把规范、代码、踩坑都放在一起讲尽量还原当时的学习路径也把网络上那些讲得含糊的地方补完整。1. 为什么我先啃 JMS 而不是直接学某个 MQ 客户端1.1 消息中间件解决的核心问题把“同步等待”变成“异步解耦”先说我当时的场景。有个订单创建服务处理完订单之后要通知库存服务扣库存、通知积分服务加积分、通知短信服务发通知。最开始写的代码很“朴素”——A 服务直接用 HTTP 调用 B 服务同步等返回。看起来逻辑很直白但一压测就出问题任何一个下游服务变慢订单接口的响应时间立刻被拖垮下游服务一重启上游的调用直接报超时订单虽然建成功了但库存、积分、短信全部没执行数据就错乱了。消息中间件解决的就是这种“你完成你的我完成我的然后把变化告诉别人”的通信问题。订单服务把“订单已创建”这个事实写到消息队列里订单服务的接口直接返回成功库存、积分、短信服务各自从队列里取消息按自己的节奏处理。这样三个下游服务谁慢、谁挂都不影响订单主流程。这个模型里有几个术语你得先建立直觉Broker是中间件服务器负责消息的存储和转发Producer是消息的生产者Consumer是消息的消费者。消息不是直接给某个消费者的而是先放到 Broker 的某个“容器”里消费者自己来取。这个“容器”就是后来的 Queue 或 Topic。1.2 JMS 是规范ActiveMQ 是实现先理解这对关系JMSJava Message Service是 Java 平台制定的一套消息服务接口规范它规定了 Java 程序怎么创建连接、怎么发消息、怎么收消息但 JMS 自己不干活真正干活的是符合这套规范的 provider比如 Apache ActiveMQ、开源的 OpenMQ 等。这个关系有点像 JDBC 和 MySQL 驱动的关系——JDBC 规定了你用什么方法连接数据库MySQL 驱动负责把这些方法翻译成 MySQL 能懂的协议。这就解释了为什么学习的时候要先看 JMS 再看 ActiveMQ如果你只对着 ActiveMQ 的 API 抄一遍换个 MQ 你就废了如果你把 JMS 的核心模型理解了接触任何一个兼容 JMS 的中间件都能很快上手。我当初踩过一个认知误区以为 ActiveMQ 提供了比 JMS 多得多的高级功能所以直接按 ActiveMQ 的文档写就行。实际上 ActiveMQ 对 JMS 规范做了很完整的实现它的核心 API 就是 JMS API你写代码时 import 的主要是javax.jms.*这个包而不是org.apache.activemq.*。后者只在构造连接工厂时才会用到。1.3 什么场景才需要引入消息中间件先泼盆冷水消息中间件不是银弹学习过程中我也遇到过滥用它的团队。一个内部查询接口没有任何瞬时流量压力硬是套一层消息队列结果多了一个运维节点、多了一段延迟、多了一堆消息乱序问题。什么时候才值得引入我的判断标准有三条一是上下游节奏不一致下游来不及同步处理需要用队列做缓冲二是下游故障不应该影响上游主流程需要隔离故障三是同一份业务事件需要被多个系统各自消费比如订单创建之后库存、积分、日志都要处理。反过来说如果只是简单的 A 调 B响应时间敏感且 B 很少故障那直接 HTTP 调用反而更合适。这部分认知虽然当时不成熟但后来回看帮我在选型时规避了不少坑。2. JMS 核心模型五个抽象角色串起全部消息逻辑JMS 1.1 里抽象出的角色其实就五个ConnectionFactory、Connection、Session、Destination、MessageProducer/MessageConsumer。剩下的概念比如 Message、AcknowledgeMode 都是在这五者之上附加的。把这五个角色串成一个故事你就能看懂 90% 的 JMS 代码。2.1 ConnectionFactory创建连接的唯一入口ConnectionFactory 是一个连接工厂接口它封装了与某个 Broker 建立连接所需的参数。在 ActiveMQ 中最常见的是ActiveMQConnectionFactory构造时传入 Broker 的地址典型格式是tcp://127.0.0.1:61616。ActiveMQConnectionFactory factory new ActiveMQConnectionFactory(tcp://127.0.0.1:61616);这个对象通常是重量级对象创建成本高所以应该复用不要每次发消息都 new 一个。我看到不少刚入门的同学把ActiveMQConnectionFactory写在方法内部短时间大量请求一来连接资源就爆炸了。正确做法是使用连接池或至少是单例持有。2.2 Connection 与 Session消息通信的骨架从工厂拿到Connection之后需要显式调用connection.start()这时候连接才真正建立。然后从 Connection 创建 Session。Session 是一个非常重要的中间层消息的生产和消费都是基于 Session 的Session 的创建方式决定了本次会话的确认模式和事务边界。Connection connection factory.createConnection(); connection.start(); Session session connection.createSession(false, Session.AUTO_ACKNOWLEDGE);createSession的第一个参数是事务标志第二参数是确认模式。在 JMS 1.1 中如果第一个参数传 true第二个参数会被忽略因为事务模式下发送和接收都通过commit和rollback控制。刚开始不用懵理解成“Session 是一段通信上下文凡是这次会话内的消息都可以在这个上下文里标记状态”就行。2.3 Destination消息要发往的地址Destination 是抽象类实际子类要么是Queue要么是Topic。Queue 对应点对点模型Topic 对应发布订阅模型。在代码里session.createQueue()或session.createTopic()创建的只是逻辑地址不需要在 Broker 侧预先建好ActiveMQ 会在第一次使用时自动创建。Queue queue session.createQueue(DEMO.ORDER); Topic topic session.createTopic(DEMO.NEWS);这里有个细节队列名称建议使用“域名前缀 业务名”的格式比如ORDER.CREATED、STOCK.DEDUCTED为了在管理后台一眼能看出用途。我见过用queue1、test2这样的名字上生产的后来排查问题差点被队友打死。2.4 MessageProducer 与 MessageConsumer实际干活的两个角色生产者和消费者都在 Session 上创建。生产者创建时可以指定默认目的地也可以不指定发送时再传目的地。生产者还支持设置是否持久化、优先级、存活时间等特性这部分会影响消息的存储行为后面展开。MessageProducer producer session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.PERSISTENT); TextMessage message session.createTextMessage(hello jms); producer.send(message);消费者既可以主动拉取消息也可以注册消息监听器异步接收消息。主动拉取用consumer.receive()它会阻塞等待可以传入超时时间异步监听用MessageListener接口Broker 一有消息就回调onMessage方法。2.5 Message 结构消息头、属性、消息体三层分工JMS 的消息对象由三部分组成。第一个是消息头header存放 JMS 规范定义的固定字段比如JMSDestination、JMSMessageID、JMSPriority、JMSExpiration。第二个是消息属性properties可以看成附加的元数据支持字符串、数字、布尔等类型常用在消息筛选和路由上。第三个是消息体body存放业务数据本身。消息体有几种类型TextMessage适合传输文本和 JSON 字符串BytesMessage适合二进制数据MapMessage适合键值对结构ObjectMessage适合序列化对象但跨语言不友好StreamMessage用得最少。实际项目里 80% 的流量都是TextMessage因为业务方一般约定用 JSON 传结构化数据不会强行依赖 Java 对象序列化。我自己的习惯是消息体只放必要的业务数据别把整个数据库实体塞进去。因为消息可能被多个系统消费每个系统关心字段不一样塞整个实体不仅体积大而且上游实体变更会导致下游反序列化失败。3. ActiveMQ 落地实操从下载启动到第一个消息跑通理论讲完了得有真家伙。我当时本地环境是 JDK 8下载的是 Apache ActiveMQ 的稳定发行版。注意 ActiveMQ 有两个分支一个叫 ActiveMQ Classic一个叫 ActiveMQ Artemis两者都是 ActiveMQ但 Artemis 是新一代架构API 上对 JMS 规范的支持更现代。作为学习 JMS 的入口我建议用 Classic因为资料多、踩坑记录全先把规范玩明白。3.1 启动 Broker十分钟确认环境下载解压后进入 bin 目录Linux/macOS 下执行./activemq startWindows 下执行activemq.bat start。启动完成后默认监听以下几个重要端口端口用途61616JMS 客户端连接的 OpenWire 端口8161Web 管理控制台端口5672AMQP 协议端口默认也开启打开浏览器访问http://127.0.0.1:8161/默认登录账号是 admin密码也是 admin进去就能看到队列列表、Topic 列表、连接数、消息统计等。我强烈建议学习阶段养成盯控制台的习惯很多代码“看起来没反应”的问题在控制台里一眼就能看出来——消息到底有没有进来、被谁消费了、有没有进入死信。3.2 第一个 JMS 生产者发送一条持久化文本消息下面这端代码是我当时跑通的第一个生产者。为了方便阅读我把异常处理简化了学习阶段先跑通生产上请务必正确封装资源关闭逻辑。import javax.jms.*; import org.apache.activemq.ActiveMQConnectionFactory; public class QueueProducer { public static void main(String[] args) throws Exception { // 1. 连接工厂指向本地 Broker ActiveMQConnectionFactory factory new ActiveMQConnectionFactory(tcp://127.0.0.1:61616); // 2. 创建连接并启动 Connection connection factory.createConnection(); connection.start(); // 3. 创建会话非事务 自动确认 Session session connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4. 创建目的地 Queue queue session.createQueue(DEMO.ORDER); // 5. 创建生产者 MessageProducer producer session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.PERSISTENT); // 6. 发送一条文本消息 TextMessage textMessage session.createTextMessage(订单已创建订单号20250101); producer.send(textMessage); System.out.println(已发送消息 textMessage.getText()); // 7. 关闭资源 producer.close(); session.close(); connection.close(); } }这个例子里最容易忽略的是connection.start()。有的教程会在创建生产者之前就调用 start有的会在发送之后才调用。start()本身不是发送动作的开关但它会让连接进入可发送/接收状态。如果你只创建连接不 start消息照样能发出去但消费者端不 start 的话就一直收不到消息。我当初就踩过这个坑后面专门讲。3.3 第一个 JMS 消费者同步接收和异步监听两种方式消费者有两种写法我建议都试一遍。第一种是主动 receive适合测试和批处理任务。import javax.jms.*; import org.apache.activemq.ActiveMQConnectionFactory; public class QueueConsumer { public static void main(String[] args) throws Exception { ActiveMQConnectionFactory factory new ActiveMQConnectionFactory(tcp://127.0.0.1:61616); Connection connection factory.createConnection(); connection.start(); Session session connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue session.createQueue(DEMO.ORDER); MessageConsumer consumer session.createConsumer(queue); // 阻塞 3 秒没消息会超时返回 null Message message consumer.receive(3000); if (message instanceof TextMessage) { System.out.println(收到消息 ((TextMessage) message).getText()); } consumer.close(); session.close(); connection.close(); } }第二种是注册监听器异步接收实际项目里这种用得更多因为它不会阻塞业务线程。示例代码我放在下面consumer.setMessageListener(new MessageListener() { Override public void onMessage(Message message) { if (message instanceof TextMessage) { try { System.out.println(异步收到消息 ((TextMessage) message).getText()); } catch (JMSException e) { e.printStackTrace(); } } } }); Thread.sleep(5000);这里有个容易翻车的地方Thread.sleep(5000)是必要的。因为main方法执行完后 Java 进程就退出了消费者监听器注册完还没来得及收消息进程就没了。看起来像是“监听器没生效”其实只是主线程提前退出。生产环境一般由应用服务器或 Spring 容器持有消费者生命周期不存在这个问题。3.4 跑完之后去控制台观察四个维度第一个消息链路跑通后我不急着写第二个 Demo而是去管理控制台把所有变化对照了一遍。四个维度最有价值Number Of Pending Messages队列里待消费的消息数量。如果这个数字一直涨说明消费端有问题。Number Of Consumers当前有多少消费者挂着。Messages Enqueued / Dequeued入队和出队的累计数能看出积压到底是多少。Blocked Time连接被阻塞的时间如果很大说明消费者处理不过来。先建立这套观察手段后面排坑会省很多时间。空口说“消息丢了”“队列卡了”很容易被运维怼把控制台截图甩过去才是正确的沟通方式。4. 队列模式和发布订阅模式看起来代码差不多语义差远了JMS 里最绕、也最影响选型的就是两种消息模型点对点模式Queue和发布订阅模式Topic。很多初学者能分别跑通但没搞明白为什么需要两种。4.1 Queue一条消息只被一个消费者取走点对点模型下一条消息只会被一个消费者成功接收。你可以启动多个消费者实例同时消费同一个队列ActiveMQ 会自动做负载均衡把不同消息分发给不同的消费者。这像是一堆快递包裹放在驿站多个配送员各自拿走一部分每个包裹最终只会被一个配送员取走。这个模型适合“任务分发”场景比如一批待处理文件10 个 worker 抢占处理比如订单状态变更库存服务和积分服务如果都要处理就不能用同一个 Queue因为消息只给一个人。这里我补充一个容易误解的点多个消费者消费同一个队列和 JMS 1.1 里的 QueueReceiver 概念有细微差别。如果两个消费者的selector条件互斥消息可能会按条件被分流但默认无 selector 时就是平均分发不是广播。4.2 Topic一条消息广播给所有订阅者发布订阅模型下一条消息会被所有“当前活跃订阅者”都收到。这像是广播喇叭谁在听谁就能听见。Topic 模式下没有“竞争消费”的概念消费者订阅同一个 Topic各自都能收到全量消息。但 Topic 有一个关键限制默认情况下非持久订阅者只在连接期间且保持订阅状态时才能收到消息。如果消费者先挂了生产者后发消息这个消费者重启之后是收不到“离线期间”的消息的。这让我当初很意外因为它和 Queue 的行为明显不一样——队列里消费者挂多久都不怕消息会积压着但 Topic 不会。要解决离线订阅丢失的问题JMS 提供了持久订阅Durable Subscription。持久订阅状态下Broker 会为这个订阅者保存未消费的消息等它上线后再推送。4.3 代码对比一条线上队列一条线上广播Queue 和 Topic 的代码结构几乎一致只有两行不同——目的地对象和创建方式。// 消费者队列模式 Queue queue session.createQueue(DEMO.ORDER); MessageConsumer consumer session.createConsumer(queue);// 消费者Topic 模式非持久订阅 Topic topic session.createTopic(DEMO.NEWS); MessageConsumer consumer session.createConsumer(topic);持久订阅的消费者写法会多一点Topic topic session.createTopic(DEMO.NEWS); // 需要为订阅者设置唯一名称 MessageConsumer consumer session.createDurableSubscriber(topic, news-consumer-1);createDurableSubscriber第二个参数是订阅 IDBroker 用这个 ID 区分不同的持久订阅者。如果你用相同的订阅 ID 连接两次第二次会替代第一次的订阅关系。发送端完全一样MessageProducer producer session.createProducer(topic); producer.send(session.createTextMessage(财经早报));4.4 选型判断别让“感觉好用”替你做决定学习期间我总结了一个很简单的判断规则同一业务事件如果多个系统需要各自独立处理用 Topic如果一个任务的多个实例竞争处理同一批消息用 Queue。举两个对比场景订单创建后触发库存扣减、积分增加、短信通知这是“一鱼多吃”三个系统各自消费同一事件应该用 Topic 或给每个消费者建独立 Queue。秒杀活动里有 50 万条抢购请求后面有 10 台幂等处理服务实例消化这是“任务竞争”应该用 Queue。更常见的企业做法是“混合使用”用 Topic 接收领域事件然后每个订阅者内部再用 Queue 做异步削峰。初学阶段先别追求花哨能把 Queue 和 Topic 的边界吃透就超过了大多数人。5. 可靠消息的底层机制确认、事务、持久化一次讲清消息中间件最常被问的一句话是“消息到底会不会丢”答案取决于三层配置消费者的确认方式、会话的事务边界、Broker 的持久化策略。每一层都直接影响消息的可靠性但要组合起来才有意义。5.1 确认模式消费者收完之后Broker 才敢删消息主动和异步监听之外Session 的确认模式决定了消息何时被“算作处理成功”。在 JMS 1.1 里有四种常见模式模式行为适用场景AUTO_ACKNOWLEDGE消费者收到消息后自动确认receive 返回或 onMessage 返回时业务处理逻辑简单、处理失败可以接受消息丢失的场景CLIENT_ACKNOWLEDGE需要手动调用 message.acknowledge() 确认处理流程较复杂希望确认时机由业务代码控制的场景DUPS_OK_ACKNOWLEDGE允许重复消息延迟确认性能好可容忍重复消费的统计类场景SESSION_TRANSACTED配合事务会话使用需要保证消息收发与业务操作原子一致的场景最常见的坑是使用 AUTO_ACKNOWLEDGE但是消费者在onMessage里处理到一半抛异常。此时消息实际上已经被确认了Broker 会把它删除重启消费者后这条消息不会再出现处理自然就丢失了。正确的做法是——如果处理失败需要重试要么用 CLIENT_ACKNOWLEDGE 模式收到消息后先不确认处理成功再调用message.acknowledge()要么开启事务处理失败就session.rollback()。CLIENT_ACKNOWLEDGE 模式有一个细节很容易忽略acknowledge()不只是确认当前这条消息而是确认这个 Session 里“已经消费但尚未确认”的所有消息。如果同时消费了多条且逐条处理调用一次 acknowledge 会把这批全确认了。要精确控制单条消息必须结合事务会话每个事务处理一条就 commit 一次。5.2 事务会话让发送和接收具备原子性connection.createSession(true, Session.SESSION_TRANSACTED)开启了事务模式。事务模式下生产者的发送动作不会立即生效而是要等session.commit()才真正把消息提交给 Broker消费者的接收动作也不会立即被确认而是等 commit 时才确认这一批消息。如果业务处理失败调用session.rollback()消息会被回滚之后可以再次消费。事务会话对“发送”和“接收”是独立的语义。比如在一个服务里先接收消息 A处理后发送消息 B然后 commit。这意味着 A 的消费确认和 B 的发送是同时生效的。这个组合非常实用相当于把“业务成功了才把消息标记为已处理”这一个要求交给中间件来保证。不过事务也不能滥用。事务越大、批量越大Broker 的锁竞争和日志压力越大。经验值是一个事务里处理的业务消息量控制在几十条以内尤其在高并发场景下过大的事务反而会成为瓶颈。5.3 持久化消息存储到磁盘进程重启才能恢复ActiveMQ 默认将消息保存在内存里同时提供 KahaDB 作为默认持久化方案它会将消息写入本地磁盘。对生产环境来说强烈建议开启持久化并且生产者发送时设置DeliveryMode.PERSISTENT。可以这样理解二者的关系持久化确保 Broker 重启后消息还在PERSISTENT 发送模式确保每条消息都走持久化存储。即使 Broker 配置了 KahaDB如果生产者发送时用的是非持久化模式消息还是可能因为 Broker 崩溃而丢失。ActiveMQ 的主配置文件中persistenceAdapter的部分默认就是 KahaDBpersistenceAdapter kahaDB directory${activemq.data}/kahadb/ /persistenceAdapter学习阶段可以刻意做一次“重启测试”发送一条持久化消息但消费者不启动然后重启 Broker再启动消费者观察消息是否还在。我第一次测的时候发现消息还在其实很不解——后来意识到这就是持久化在起作用。5.4 组合起来可靠消息的生产端配置怎么开把三层机制组合起来一个稳妥、可靠的生产者配置看起来是这样Session session connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.PERSISTENT); producer.setTimeToLive(60000); // 消息有效期为 1 分钟 producer.send(message);消费者侧如果对处理结果有严格成功要求通常选择事务会话Session session connection.createSession(true, Session.SESSION_TRANSACTED); MessageConsumer consumer session.createConsumer(queue); TextMessage msg (TextMessage) consumer.receive(); // 处理业务逻辑比如更新数据库、调用外部接口 // 都成功后提交 session.commit(); // 失败则回滚 session.rollback();这里涉及一个进阶考量如果消费者处理到一半进程崩了事务尚未 commitBroker 会重新投递这条消息。重新投递可能导致重复消费所以消费者的业务逻辑必须做幂等。消息中间件本身并不能彻底避免重复投递它只能保证“至少一次”不能保证“正好一次”。这句话我建议所有初学者刻在脑子里。6. 学习过程中实际踩过的四个坑这部分是我最想写的。Demo 都能跑通但生产要素一进来问题就花样百出。下面四个坑都是我真实遇到过的按照排查链路写出来方便你以后对着复现。6.1 第一个坑启动 Broker 就报端口占用现象执行./activemq start后控制台报java.net.BindException: Address already in use: JVM_Bind。排查我把堆栈里暴露的端口号 61616 用lsof -i:61616查了下发现是本机之前跑过一个没关干净的 ActiveMQ 实例。这提醒我两个习惯一是问题定位要先确认端口占用而不是盲目重装二是启动前用./activemq stop或jps查看已有 Java 进程别把多个 broker 实例混在一起跑。如果你是在同一台机器上跑多个 broker 做集群学习必须修改conf/activemq.xml里的 transportConnector 端口还要调整conf/jetty.xml里的管理端口否则必然冲突。6.2 第二个坑队列里明明有消息消费者却收不到现象生产者发送成功控制台里Number Of Pending Messages是 5但消费者启动后一条都没收到。排查过程我先确认消费者确实连接上了 broker——控制台的 Connections 页面能看到消费者连接。然后检查消费的队列名发现生产者和消费者用的队列名不一致。这个属于低级错误。但还有一个更隐蔽的原因困扰了我更久消费者连接没调用connection.start()。我照着网上某篇文章只创建了 connection 和 session没有显式调用 start。这个操作在“异步监听 等待”的例子中看起来没问题但实际消息分发一直没有真正开始。补上connection.start()后一切正常。这个教训让我从此养成了一个习惯JMS 样板代码一定逐行核验别漏掉生命周期方法。6.3 第三个坑发布订阅模式下“先启动生产者后启动消费者”丢消息现象我用 Topic 建模一个广播消息先运行生产者发了两条消息再启动消费者结果消息一条都没收到。排查过程这其实是 Topic 模型的正常行为不是 bug。非持久订阅者不会收到订阅之前的消息。我当时误以为 Topic 和 Queue 一样消息会先积压在 broker 里消费者一来就全推给它。搞清楚这个机制后我重新设计了一版客户端先启动消费者并保持在线再触发生产者发送消息就正常收到了。如果业务上确实需要“离线订阅不丢消息”就要在消费端使用createDurableSubscriber。注意持久订阅的订阅 ID 一旦创建就不能随意变更否则旧的持久订阅消息将无法再被消费会一直堆积在 broker 中。6.4 第四个坑重复消费引发的脏数据问题现象消费者消费消息后更新了数据库但进程在数据库事务提交后、Session 提交前崩溃了重启后同一消息被再次消费数据库被更新了两次。排查过程当时我用了事务会话以为有事务就万事大吉。实际上事务会话保证的是“消息确认和业务处理的原子性”但“业务处理”本身如果不在同一个数据库事务里状态就无法对账。后来我用了一个幂等方案消息体带业务主键消费者在处理前先查一下数据库是否已有对应记录有就直接返回没有才执行业务并落库且主键唯一索引兜底。这样即使消息重复投递也不会产生重复数据。这个坑给我最大的启发是消息中间件只保证传输层面的至少一次投递消费端必须自己做好幂等。你以为中间件帮你解决了所有问题它只是帮你把问题限定在了一个可控范围内剩下的还要靠自己的业务设计。7. 学习收尾时的一些心得过完前面这六个章节后我又重新回头看了最初促使我开始学习的那个“同步调用雪崩”场景。用上 ActiveMQ 之后其实还没有结束还配套做了消费并发度控制、数据库连接数控制、失败重试队列、监控告警才敢说这个链路基本可靠了。这个认知我觉得比学会 API 本身更重要消息中间件解决的是通信模式问题但一个系统的稳定性永远要靠整体设计不会因为引入一个组件就自动变好。如果你正在学 JMS 和 ActiveMQ我给三条很实在的建议。第一条不要只看着我的代码抄一定自己把生产者、消费者、管理控制台三样对起来跑通一个完整的收发。第二条把 Queue 和 Topic 的区别多做几个实验包括离线订阅、多个消费者消费同一个队列、持久订阅恢复这些体验比背概念管用太多。第三条第一次接触消息可靠性的时候强制自己回答“消息什么时候会丢、重复消费怎么兜底、中间件重启后消息还在不在”这三个问题回答完再继续深入事务和持久化否则知识很容易停留在 API 层面。最后分享一个小技巧学习期间可以在 ActiveMQ 的配置里临时把日志级别调成 DEBUG重点看 messages 相关日志和 session 相关日志。以前我一直觉得中间件日志太多不看后来排查那两次诡异问题时DEBUG 日志里的 broker 内部流转信息帮了大忙。日志是排查问题最早出手的武器不要等到控制台里消息都诡异了才想起来翻日志。