news 2026/10/20 6:53:51

消息中间件选型与ActiveMQ落地:从JMS规范到连接池、可靠性及踩坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
消息中间件选型与ActiveMQ落地:从JMS规范到连接池、可靠性及踩坑实践

做消息中间件选型的时候,很多人第一反应是上大厂那套重家伙,好像不搞个分布式集群就没法跟人打招呼。我这次倒是反着来:因为一个积分系统要异步接入订单系统,订单高峰期HTTP回调经常超时,重试又导致下游重复记账,最后只能老老实实把JMS和ActiveMQ从概念到落地完整过了一遍。学完之后有个很深的体会——JMS作为Java世界的消息规范,和ActiveMQ这个具体实现的关系,特别像JDBC和MySQL的关系。搞懂这层关系,后面写代码、排问题都会顺很多。

这篇文章就是把这趟学习过程整理出来,适合刚接触消息队列、准备在项目里落地ActiveMQ的开发者。我会从“为什么需要它”讲到“API模型怎么理解”,再到“安装部署、双端通信代码、可靠性和事务细节”,最后把实际排查中遇到的几个坑完整复盘一遍。内容以实践为主,不堆概念。

1. 一段真实业务场景:为什么最终选了JMS + ActiveMQ

1.1 积分服务和订单服务之间的“重试风暴”

先说业务背景。模拟项目里有一个订单服务和一个积分服务,订单创建成功后要调用积分服务给用户加积分。最开始直接走HTTP同步调用,逻辑很简单:订单落库,调积分接口,返回成功就算完。坏就坏在高峰期——积分服务偶尔抖动,一个请求超时,调用方就重试;重试又叠加上来,积分服务被打得更慢,形成恶性循环。我翻日志的时候见过同一笔订单在5分钟内被重试了17次。

同步调用是典型的“强耦合、无缓冲”:上游要等下游返回结果,下游一慢,上游整个请求链路都拖慢。更麻烦的是订单服务多实例部署时,每个实例都在重试,流量被放大了好几倍。这时候需要的其实不是更强的服务器,而是一个“缓冲层”——订单服务把消息发出去就返回,积分服务按自己的速度去消费,重试和削峰的事情交给消息中间件处理。

1.2 为什么要按JMS规范选型,而不是直接绑定实现

选型的时候,我对比了几个Java生态里常见的消息中间件。Kafka偏向日志流、高吞吐场景,但部署和运维成本高;另外两个也都不错,但项目的规模实在不需要那么重的集群。最终选ActiveMQ,其中一个关键原因是它完整实现了JMS规范,也就是说你写的生产者和消费者代码,依赖的是javax.jms包下的标准接口。哪天团队觉得ActiveMQ撑不住了,换成其他兼容JMS规范的产品,业务代码改动量会小很多。

这跟JDBC一个道理:应用层写java.sql.Connection,底层换成MySQL还是别的数据库,驱动换掉就差不多了。抽象层最大的价值不是让代码少写几行,而是把“业务逻辑”和“具体中间件”之间做一个隔离,出问题的时候有一个清晰的替换边界。另一个原因是ActiveMQ部署足够轻,单机解压就能跑,功能上队列、主题、持久化、事务、消息过滤都有,非常适合中小规模项目。

1.3 什么场景不该硬上ActiveMQ

学习过程中我也梳理了反面清单。如果项目就是两个服务之间的简单通知,一天几百条调用,同步接口完全能扛住,引入消息队列反而多出一个需要维护的组件。如果业务要求强实时、强一致,比如支付成功后要立刻拿到结果展示给用户,那MQ的异步特性就不合适,还是要走同步接口加事务。

我的建议是,先想清楚问题是不是“削峰填谷、解耦、异步化”这三类。如果是,MQ是合适的;如果不是,别为了技术栈好看而上消息中间件。表格总结一下:

场景特征是否适合引入ActiveMQ原因
接口间需要异步解耦,下游处理慢适合上游无需等待,削峰
高峰期流量集中,存在瞬时压力适合队列缓冲,控制消费速率
广播通知、事件分发适合Topic模型天然支持发布订阅
调用量极小,同步两三百毫秒内完成不适合增加维护成本和链路复杂度
要求立即拿到最终结果不适合异步无法保证实时返回

2. JMS核心对象模型:从Connection到Message的完整链条

2.1 JMS和ActiveMQ的“规范与实现”双层结构

JMS全称Java Message Service,是Java平台上定义的一套消息服务标准接口,规定了生产者和消费者怎么创建连接、怎么收发消息、消息格式长什么样。但这些接口本身不能干活,真正干活的是像ActiveMQ这样的消息中间件,它实现了这套接口,并提供底层的Broker服务。

初学者最容易绕晕的就是这里:代码里import的全是javax.jms开头的包,好像没直接用到ActiveMQ的类,但跑起来链路明明就是通过ActiveMQ的Broker在传输消息。其实工厂类ActiveMQConnectionFactory是ActiveMQ自己实现的,它构造出来的Connection、Session、MessageProducer等对象,都是在JMS标准接口背后做了具体实现的。所以学习的时候,我建议先把JMS的接口模型搞清楚,再去看ActiveMQ的配置和参数,就不会被代码里的包名迷惑了。

2.2 对象模型的完整链条和生命周期

一条完整的好消息链路大致是这个顺序:先创建连接工厂,由工厂创建连接,连接创建会话,会话再创建消息生产者和消费者,然后通过Destination(目的地)来指定消息发到哪里。用生活里的类比就是:ConnectionFactory是快递公司的加盟网点,Connection是快递员,Session是快递员手里的工作单,Destination是收货地址,MessageProducer是把包裹交给快递员的动作,MessageConsumer是收件人签收的动作。

有一个特别容易踩的坑是Session的使用边界。JMS规范里Session不是线程安全的,一个Session建议只在一个线程里使用。很多新手把Session定义成全局单例,然后开一堆线程同时往里塞消息,结果就出现偶发性的消息丢失或者报错。后面踩坑章节我会专门复盘这个问题。另一个要注意的是Connection和Session都要有明确的关闭动作,顺序上先关生产者、消费者,再关Session,最后关Connection,尽量在finally块里执行。

2.3 消息的几种类型,到底该怎么选

JMS定义了五种消息类型:TextMessage、MapMessage、BytesMessage、ObjectMessage、StreamMessage。我自己的使用习惯是这样的:

  • TextMessage最常用,传JSON字符串非常舒服,前后端、跨语言都方便。
  • MapMessage适合传结构化键值对,但键类型受限,灵活性一般。
  • BytesMessage适合传二进制内容,比如文件分片、图片数据。
  • ObjectMessage理论上能传Java对象,但我基本不推荐,序列化和版本兼容问题多。
  • StreamMessage是流式读写,实际项目用得少。
类型适合场景注意点
TextMessageJSON、XML、纯文本最通用
MapMessage固定键值结构键值受限
BytesMessage二进制文件需自己处理字节流
ObjectMessageJava对象传输类版本变化会影响反序列化
StreamMessage流式原语使用较少

文本消息的好处是后面做消息格式升级方便,消费方拿到字符串自己解析,兼容老格式就是代码里多几个判断的问题。换成ObjectMessage,老版本对象反序列化失败那就是事故了。

2.4 Queue与Topic:点对点和发布订阅的关键差异

Queue对应点对点模型(Point-to-Point),一条消息只会被一个消费者消费。哪怕你有十个消费者在监听同一个Queue,这条消息也只会发给其中一个。消费完成后消息就被移出队列,不存在“群发”的效果。Topic对应发布订阅模型(Pub/Sub),生产者发布一条消息,所有订阅了这个Topic的消费者都能收到。

初学者经常在这一步栽跟头:用Topic就是想做广播,但广播要求消费者必须保持在线订阅状态,离线期间的Topic消息是收不到的,除非配了持久订阅。Queue则相反,消息只要到达Broker就会持久留存(配置持久化后),消费者后启动也能补收。理解这个差异,后面很多“消息不见了”的问题都能避免。

3. ActiveMQ落地:安装、启动与第一个消息收发

3.1 本地环境快速跑起一个Broker实例

ActiveMQ的安装没有太多花活,官网下载压缩包,解压后进到bin目录,Windows下运行activemq.bat start,Linux下运行./activemq start,默认就起来了。启动后默认监听61616端口,这是OpenWire协议端口,客户端连接主要走这个端口。另一个是8161端口,这是Web控制台端口,浏览器打开能看消息的收发量、消费者状态,还有简单的消息浏览功能。

我第一次启动的时候卡了一个小问题:控制台打不开,后来发现是防火墙把8161端口挡了。还有一次是启动脚本提示端口被占用,用lsof -i:61616查了一下,发现之前有个残留的Java进程没退干净。如果是第一次玩,建议先用默认配置跑通,熟悉了再改conf/activemq.xml。控制台默认账号通常是admin/admin,但生产环境要第一时间改掉,这个后面再说。

3.2 Maven依赖和最小生产者代码

新建一个工程,加两个依赖就够:

<dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-client</artifactId> <version>5.18.2</version> </dependency> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-pool</artifactId> <version>5.18.2</version> </dependency>

activemq-client是核心客户端库,activemq-pool后面做连接池会用。生产者的代码其实很短:

public class SimpleProducer { public static void main(String[] args) throws JMSException { // 1. 创建连接工厂,Broker地址为本地 ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://localhost:61616"); // 2. 创建连接并启动 Connection connection = factory.createConnection(); connection.start(); // 3. 创建会话,第一个参数表示是否开启事务,这里用false Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4. 创建消息目的地 Queue queue = session.createQueue("test.queue"); // 5. 创建生产者 MessageProducer producer = session.createProducer(queue); // 6. 发送消息 TextMessage message = session.createTextMessage("hello activemq"); producer.send(message); System.out.println("消息发送完成"); // 关闭资源 producer.close(); session.close(); connection.close(); } }

这个流程就是JMS标准链路的标准写法。注意connection.start(),有些同学会漏掉,结果生产者能发但消费者收不到消息,其实是因为连接没启动导致消费者无法接收。

3.3 最小消费者代码:同步接收和异步监听

消费者的写法有两种。一种是主动拉取,调用receive()方法阻塞等待消息;另一种是注册一个监听器,Broker有消息来了主动回调。先看同步接收:

public class SimpleConsumer { public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory( "admin", "admin", "tcp://localhost:61616"); Connection connection = factory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue("test.queue"); MessageConsumer consumer = session.createConsumer(queue); // 阻塞接收,设置超时时间 Message message = consumer.receive(1000); if (message instanceof TextMessage) { TextMessage textMessage = (TextMessage) message; System.out.println("收到消息: " + textMessage.getText()); } consumer.close(); session.close(); connection.close(); } }

同步接收适合需要控制节奏的场景,比如批量拉取任务。异步监听适合事件通知类业务:

consumer.setMessageListener(new MessageListener() { @Override public void onMessage(Message message) { if (message instanceof TextMessage) { try { TextMessage textMessage = (TextMessage) message; System.out.println("异步收到消息: " + textMessage.getText()); } catch (JMSException e) { // 这里的异常处理非常关键 e.printStackTrace(); } } } });

异步监听里最忌讳的就是onMessage方法里抛异常后什么都不处理。默认情况下AUTO_ACKNOWLEDGE模式会在onMessage正常返回后自动确认消息,如果抛了异常且未确认,消息会被重新投递,极容易造成无限重发循环。这个我放到踩坑部分详细说。

3.4 用控制台验证消息是否正常

跑完上面两段代码后,打开8161控制台,找到对应的Queue。有两个计数值得关注:Enqueued表示进入队列的消息总数,Dequeued表示消费完成移出队列的消息数。两者相等说明消息都消费完了。如果Dequeued远小于Enqueued,说明有消息积压或者消费者没消费。这个页面就是后面排查问题时的第一现场。

4. 双端通信实战:连接池、failover与JmsTemplate封装

4.1 为什么不能用裸Connection到处创建

前面示例为了方便,每个消息都直接new一个Connection,这在demo里没问题,但真实项目里不能这么干。建立一条连接,底层要创建TCP连接、完成认证握手、创建传输通道,成本不低。如果每次发消息都现场创建、用完关闭,高并发下Broker会收到大量无意义的建连请求,消费端连接数也容易被打满。

解决办法就是像数据库连接池一样引入ActiveMQ连接池。activemq-pool提供了PooledConnectionFactory,可以复用连接和会话。配置了几个关键参数之后,整个资源管理就自动了,用完归还,不频繁建连。

ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory( "tcp://localhost:61616"); // 连接池工厂 PooledConnectionFactory pooledFactory = new PooledConnectionFactory(factory); // 池中最多多少个连接 pooledFactory.setMaxConnections(10); // 每个连接上最多活跃的会话数 pooledFactory.setMaximumActiveSessionPerConnection(100); pooledFactory.start();

这里的maxConnections和数据库连接池的maximumPoolSize一个意思。可以根据业务并发量估算:平均延迟50毫秒,单连接撑住的并发QPS大概也就几十,要支撑更高的并发,就要多几条连接来分流。

4.2 failover协议:连接断了怎么办

单台Broker如果挂了,客户端连接会直接断掉,生产环境必须有高可用手段。ActiveMQ的客户端支持failover协议,连接串写成这样:

failover:(tcp://192.168.1.10:61616,tcp://192.168.1.11:61616)?randomize=true&initialReconnectDelay=100&maxReconnectAttempts=10

多个地址之间用逗号分隔,一旦当前节点不可用,客户端会自动重连到另一个节点。randomize=true表示客户端随机选择初始连接节点,避免所有客户端都连到同一台机器上,实现简单的负载分散。initialReconnectDelay是第一次重连的延迟毫秒数,maxReconnectAttempts是最大重连次数,超过后就会抛出异常。

这个方案适合主从、集群部署的场景。当然,failover只是客户端层面的自动重连,Broker之间的数据同步还要靠后端的网络配置和持久化机制,不能只靠一个URL就解决所有高可用问题。

4.3 封装一个通用消息发送类

实际项目中消息发送一般会统一封装成一个组件,避免每个业务方都去操作底层JMS API。我用JmsTemplate做过一个简单的封装:

@Component public class JmsMessageSender { private final JmsTemplate jmsTemplate; public JmsMessageSender(ConnectionFactory connectionFactory) { this.jmsTemplate = new JmsTemplate(connectionFactory); this.jmsTemplate.setDeliveryMode(DeliveryMode.PERSISTENT); } public void sendQueue(String queueName, String message) { jmsTemplate.send(queueName, s -> s.createTextMessage(message)); } public void sendTopic(String topicName, String message) { jmsTemplate.send(topicName, s -> s.createTextMessage(message)); } public void sendWithTimeout(String queueName, String message, long timeToLive) { jmsTemplate.setTimeToLive(timeToLive); jmsTemplate.convertAndSend(queueName, message); } }

这个类将“目的地名称”和“业务消息内容”隔离开,业务方只需要调用方法传入队列名和消息体就行,至于连接从哪里来、会话怎么管理、消息确认怎么设置,全部由模板完成。DeliveryMode.PERSISTENT表示消息要持久化,Broker收到后会先落盘再返回成功,保证Broker重启后消息不丢。代价是性能会差一些,适合重要业务;如果是日志类可容忍丢失的数据,可以改成NON_PERSISTENT换取更高的吞吐。

4.4 请求-响应模式:TemporaryQueue的玩法

除了发完就完事的消息,JMS还支持同步请求-响应模式。客户端发送消息后,可以通过临时队列等待服务端处理结果。临时队列是Connection级别独立的,消费端可以获取到请求消息里的JMSReplyTo字段,把结果发回这个临时队列。

// 生产端 Queue replyQueue = session.createTemporaryQueue(); MessageProducer producer = session.createProducer(requestQueue); TextMessage textMessage = session.createTextMessage("查询库存"); textMessage.setJMSReplyTo(replyQueue); producer.send(textMessage); MessageConsumer consumer = session.createConsumer(replyQueue); Message reply = consumer.receive(3000); if (reply != null) { System.out.println("响应内容: " + ((TextMessage) reply).getText()); }

这是个很有用的模式,比如调用一个慢服务时,不想同步等待太长时间,就把请求放到消息队列里,让服务端异步处理完再通过临时队列回传结果。但要注意设置超时时间,万一服务端挂了,客户端不能被永久卡死。临时队列还有个特点,Connection一断它就被销毁,所以这个模式不太适合Broker故障恢复的场景,使用前要权衡好。

5. 可靠性细节:持久化、事务、确认机制与死信处理

5.1 Broker端持久化存储:kahadb、jdbc store怎么选

ActiveMQ的持久化主要解决“Broker重启之后消息还在不在”的问题。默认用的是kahadb,这是一个基于文件追加日志的存储引擎,性能不错,吞吐量高,适合绝大多数场景。配置在activemq.xml里,默认就会启用。如果消息量特别大,也可以改成jdbc store,把消息存进数据库,好处是可以用数据库的备份、恢复方案,坏处是性能通常比文件型存储低不少。

从可靠性的角度,我建议把这几个层次理解清楚:

存储类型特点适合场景
kahadb默认,文件型,性能好通用
jdbc store落库,管理方便,性能略差已有数据库运维体系的团队
amq store老版文件存储不推荐新项目使用

持久化不只是Broker端的事,消息本身也要标记为持久化。DeliveryMode.PERSISTENT配合Broker持久化,才能保证消息在Broker重启后不丢。如果哪一层没做好,重启后消息就可能消失。

5.2 事务模式:生产者事务与消费者事务

Session的第一个参数是transacted,设置为true就表示开启事务。生产端开启事务后,可以连续发送多条消息,最后调用session.commit()统一提交。只有提交成功,消息才会真正入队。如果中途出错,调用session.rollback(),这批消息全部撤销。

消费端的事务和确认机制挂钩。消费端开启事务后,消息处理完成需要commit(),消息才算被确认消费;处理失败可以rollback(),消息会被重新投递。我写消费端代码的习惯是:

Session session = connection.createSession(true, Session.SESSION_TRANSACTED); try { Message message = consumer.receive(1000); // 处理业务逻辑 processBusiness(message); session.commit(); // 处理成功,确认提交 } catch (Exception e) { session.rollback(); // 处理失败,回滚重投 throw e; }

事务很有用,但也别把整个业务逻辑塞进来。事务范围越大,锁和冲突的可能性越大。这个道理和数据库事务是一模一样的:要短、要快、要明确边界。

5.3 四种确认模式:各自解决什么问题

说到消费端确认模式,JMS提供了四类,但都是围绕“什么时候告诉Broker可以删掉这条消息”这一件事。

确认模式时机风险
AUTO_ACKNOWLEDGE消息接收成功或回调返回时自动确认可能出现接收后、处理前崩溃导致消息丢失
CLIENT_ACKNOWLEDGE代码里手动调用message.acknowledge()忘记调用就永远不确认,消息积压
DUPS_OK_ACKNOWLEDGE允许重复确认,性能高可能重复消费
SESSION_TRANSACTED配合事务Sessioncommit才算确认,rollback重新投递

我的建议是,核心业务尽量用CLIENT_ACKNOWLEDGE或者事务模式。因为AUTO_ACKNOWLEDGE的确认时机是在消息被消费方接收后立刻确认,如果那一瞬间消费方宕机,消息已出队列但业务还没处理完,重启后这条消息就彻底丢了。手动确认可以把“确认”延迟到业务处理完之后,牺牲一点复杂度换取可靠性。

5.4 死信队列和RedeliveryPolicy:一条消息最多重试几次

ActiveMQ默认有一条叫ActiveMQ.DLQ的死信队列。消息消费失败、需要重新投递的次数超过阈值后,会被移入死信队列,不再骚扰正常消费流程。默认情况下,一条消息最多重投6次。这个次数可以配置:

<policyEntry queue=">"> <redeliveryPolicy> <property name="maximumRedeliveries" value="3"/> <property name="initialRedeliveryDelay" value="1000"/> </redeliveryPolicy> </policyEntry>

initialRedeliveryDelay是延迟多久再投递,避免失败后马上重投把无效请求打爆。死信队列的价值在于:它把“失败的终究会失败”的消息隔离出来,运维人员可以去查为什么失败、能不能人工修复后重新处理,而不是让系统无限重试。

5.5 消费端幂等:消息重复是一种常态

讲了这么多可靠性机制,我必须强调一点:在高可用体系里,消息重复消费比消息丢失更常见、也更难缠。比如消费端处理了消息、业务写库成功,但ack超时了,Broker以为没消费,又把同一笔消息重新投递。如果消费端的接口不是幂等的,库存就扣了两次,账就对不上了。

最简单的解法是给业务消息加一个全局唯一消息ID,消费端用一个去重表记录已经处理过的ID,遇到重复消息直接跳过。消息ID可以用订单号、业务主键、或者UUID,只要能保证全局唯一即可。这个设计不复杂,但能避免非常多的对账噩梦,值得在项目初期就做进去。

6. 踩坑实录:消息丢失、重复消费与堆积排障链路

6.1 坑一:消息发完之后“消失”了,问题出在Topic订阅方式

现象:生产者是先启动的,发了一条Topic消息,日志显示发送成功。之后消费者启动并订阅了这个Topic,结果一条消息都收不到。

排查过程:先到控制台看Topic的Enqueued计数,确认消息确实进入了Topic。然后排查消费者订阅方式,发现用的是非持久订阅。这就命中了我前面说的点:订阅者在线时才收得到消息,离线期间发布的Topic消息直接被丢弃。如果希望离线订阅者也能收到,有两个方案:一是权衡业务场景,改用Queue;二是用持久订阅(DurableSubscription)。Topic和Queue最大的区别就在这里,千万别拿Queue的思维套到Topic上。

最终结论:这不是ActiveMQ的问题,是模型理解错位。把场景改成事件通知时,要确认是否有消费者离线补偿的需求。如果有,用Queue或者持久订阅;如果没有(比如是一个实时行情通知,错过就算了),用普通Topic即可。

6.2 坑二:消费端莫名重复消费,问题出在AUTO确认和长耗时处理

现象:明明业务已经处理完成,但日志里看到了同一条消息被消费了两次。排查了很久找不到原因。

排查链路:查看消费端处理逻辑,发现onMessage里处理了一条耗时较长的业务,比如调外部接口要3秒。而这时的确认模式是AUTO_ACKNOWLEDGE,消息在接收时就被预确认了,如果处理时间过长,Session与Broker之间的ack状态出问题,Broker可能认为消息未被消费而重新投递。另外如果prefetch消息数量不为1,消费端会一次性拉取多条,处理完时统一确认,这中间消费端一旦宕机,整批消息全部重新投递,重复消费的范围会更大。

解决方法分两步:确认模式改为CLIENT_ACKNOWLEDGE,处理完业务后手动调用acknowledge();同时把消费端的prefetchSize调成1,让每条消息独立确认。

ActiveMQPrefetchPolicy prefetchPolicy = new ActiveMQPrefetchPolicy(); prefetchPolicy.setQueuePrefetch(1); factory.setPrefetchPolicy(prefetchPolicy);

当然,最稳妥的仍是幂等设计,因为无论如何都有极小的概率重复投递,代码里做一层去重,才能彻底兜底。

6.3 坑三:队列堆积告警,慢消费者把Broker内存吃满

现象:某个Queue的Pending消息数从几百涨到几十万,消费速率远低于生产速率,控制台显示Broker内存使用率持续升高,其他队列的消费也收到影响。

排查过程:先看消费端日志,发现单个消息的处理时间平均从50毫秒涨到了500毫秒以上,原因是有个查询走了全表扫描,数据库慢查询拖累了主流程。这是典型的慢消费者问题。消费者的消费速率不匹配生产速率,消息堆积是必然结果。这时候单纯加消费者实例的确能提升并行消费能力,但根本性问题在于处理链路本身太慢。

另一个隐藏原因是默认prefetch值过大。ActiveMQ默认的队列预取数是1000,也就是消费者一次拉取1000条保存在本地内存里慢慢处理。如果消费者内存配得不大,这1000条就可能撑爆堆内存。对策是减小prefetch值,类似setQueuePrefetch(10),同时保证消费端处理逻辑快速退出,不要让一条消息的处理时间无限制拉长。

这次排查给我的教训:队列积压最先怀疑的永远是消费端,不是Broker。控制台看一眼Dequeued速率就知道问题了,别一开始就怀疑中间件配置。

6.4 坑四:多线程共享同一个Session,偶发性JMSException

现象:项目里为了加速消费,一个消费者组里开了多个线程,共用同一个Session去创建Consumer,运行一段时间后偶尔抛JMSException,提示Session正忙或已关闭。

排查链路:查JMS规范,里面明确写了Session不是线程安全的。一个Session就要在一个专用线程里使用,多线程共享时,底层OpenWire协议发送的packet顺序会乱,就会出现偶发报错。解决方案是每个消费者线程创建独立Session,或者用连接池的PooledSession来替代。用连接池之后,每次从池里借一个Session,用完了归还,天然规避了这个问题。

这一点其实是最容易被忽视的:JMS的接口看起来很安全,实际上坑藏在并发模型里。任何要共享的资源都要先问一句:文档里有没有说明线程安全?没有说明的默认当成不安全处理。

最后说一句我个人的体会。学ActiveMQ如果只看文档,很容易记了一堆名词却不知道怎么用;真正有效的做法是先把JMS的接口模型画一遍,然后拿一个最简单的收发demo跑通,再往里面加连接池、事务、持久化这些参数。每加一个特性,都要到控制台或者日志里验证它的影响是什么。遇到问题时别急着改配置,先顺着消息流转的链路走一遍,往往很快就能定位到“模型理解错位”还是“资源使用方式不对”。整个过程其实不复杂,但确实需要耐心。

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

ISO 12207:2017软件生命周期标准落地指南:过程裁剪与研发流程映射

简介&#xff1a;ISO/IEC/IEEE 12207:2017《系统与软件工程——软件生命周期过程》完整英文电子版&#xff0c;由ISO、IEC与IEEE联合发布&#xff0c;面向软件工程从业者、系统架构师、质量与过程管理人员及高校师生&#xff0c;用于建立软件从概念到废弃的全生命周期过程框架。…

作者头像 李华
网站建设 2026/10/20 6:52:25

代码评审工作流设计:从人工走查到自动化质量门禁的工程实践

过去半年&#xff0c;我把团队代码评审这件事从头到尾拆了一遍&#xff0c;重新设计、落地、反复踩坑&#xff0c;最后沉淀成一套开源的工作流&#xff0c;名字就叫 open-code-review。它不是什么颠覆性黑科技&#xff0c;而是一套把代码审查从“走形式”变成“真把关”的工程实…

作者头像 李华
网站建设 2026/10/20 6:51:53

AI业务应用交付:从业务诊断到产品经营的全周期方法论

1. 这不是AI项目交付&#xff0c;是业务价值交付——先拆穿三个普遍误解“AI业务应用交付”这个词&#xff0c;最近在不少团队的OKR里高频出现。但翻看实际落地的案例&#xff0c;我见过太多项目卡在“模型准确率98%”却没人用、“系统上线三个月后日活归零”、“业务方说效果不…

作者头像 李华
网站建设 2026/10/20 6:51:46

配置文件全生命周期管理实战:从格式选型到事故排查

config 不是小事&#xff1a;一份关于配置文件全生命周期管理的实战复盘入行这些年&#xff0c;我见过太多项目因为"config"这两个字翻车&#xff1a;某天改了一个不起眼的参数&#xff0c;服务起不来了&#xff1b;同一个配置在开发环境跑得好好的&#xff0c;上到灰…

作者头像 李华
网站建设 2026/10/20 6:51:24

新能源电池维修核心:高压模块平衡机如何破解电池均衡难题

做新能源维修这几年&#xff0c;我越来越觉得“均衡”这两个字才是电池组的灵魂。很多车续航断崖式下跌、快充老是提前跳枪、冬天一冷就报动力电池故障&#xff0c;拆开BMS一查&#xff0c;十有八九不是电芯坏了&#xff0c;而是某一串电压掉队&#xff0c;整个电池包被这一颗“…

作者头像 李华
网站建设 2026/10/20 6:51:14

桌面端AI Agent正在重构教育场景:从备课到答疑的落地实践

与几位做教育信息化的老朋友聊天时&#xff0c;一个感受越来越强烈&#xff1a;2026年可能真是桌面端AI Agent在教育领域的一个分水岭。不是因为技术突然有了什么突破性新闻&#xff0c;而是因为在真实的教学场景里&#xff0c;网页版大模型的使用瓶颈已经暴露得足够充分&#…

作者头像 李华