如何在批量创建 `DefaultMessageListenerContainer` 时避免阻塞问题

大家好,我是G探险者!

今天记录一个for循环被阻塞导致后面循环无法执行的问题。

1. 背景问题

在一个分布式消息系统中,我们通常会使用 Spring JMS 提供的 DefaultMessageListenerContainer(简称 DMLC)来监听消息队列。

一个常见的场景是:

  • 系统中存在 多个 MQ 连接工厂(比如不同租户、不同集群、不同数据源的 MQ)。
  • 需要在程序启动时,对这些连接工厂循环创建独立的 DMLC,分别监听对应的队列。

伪代码如下:

java 复制代码
for (ConnectionFactory factory : connectionFactoryList) {
    DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
    container.setConnectionFactory(factory);
    container.setDestination(...);
    container.setMessageListener(...);
    container.initialize();
    container.start(); // 启动监听容器
}

在正常情况下,这段代码会顺利执行,所有容器都能依次启动。


2. 问题产生

然而在实际运行过程中,有一个棘手的问题:

  • 如果某个 MQ 工厂对应的 Broker 不可用 ,比如因为网络异常、Broker 宕机,或者是常见的 2009 连接异常 (IBM MQ 场景下),那么 DMLC 在 start() 阶段就会阻塞,长时间无法完成启动。
  • 这会导致 for 循环卡死在这一轮,后续的容器创建都无法继续进行。

👉 换句话说,一个坏的 MQ 连接会拖死整个初始化流程


3. 解决思路

为了解决这个问题,我们的目标是:

  • 即使某个 DMLC 启动失败,也不能阻塞整个循环;
  • 最好是让它 异步尝试启动,失败了可以靠自身的自动重连机制继续恢复。

于是就有了一个思路: 把每个容器的启动逻辑放到单独的线程中执行 ,而不是在 for 循环中同步执行。


4. 异步启动容器的实现

我们可以使用 ExecutorService 来包装启动逻辑:

java 复制代码
ExecutorService executor = Executors.newCachedThreadPool();

for (ConnectionFactory factory : connectionFactoryList) {
    executor.submit(() -> {
        try {
            DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
            container.setConnectionFactory(factory);
            container.setDestination(...);
            container.setMessageListener(...);
            container.afterPropertiesSet(); 
            container.start(); // 独立线程中启动
            // 这里可以保存容器引用,用于后续stop
            System.out.println("容器启动成功: " + factory);
        } catch (Exception e) {
            // 打印异常,但不会影响for循环继续
            System.err.println("容器启动失败: " + factory + ", error: " + e.getMessage());
        }
    });
}

这样,即使某个容器因为 MQ 不可达而长时间卡住,也只会阻塞自己的线程,不会拖慢 for 循环

主线程可以很快完成整个循环,所有容器的启动任务被丢给线程池来异步执行。


5. 原理解释:为什么这样能避免阻塞?

关键点在于线程模型的变化:

  • 原始写法for 循环中的 container.start()同步调用,如果阻塞,主线程就被卡住。
  • 改进写法 :每个 container.start() 都被放入线程池,由线程池里的线程去执行;而主线程仅仅是 submit() 任务,不会等待结果。

因此,阻塞被局限在子线程内,不会影响整个循环的继续执行

这就是"把启动变成了异步"的根本原因。


6. 线程池的销毁问题

由于我们使用了 ExecutorService,需要注意线程池的生命周期:

  • 在容器关闭时,记得调用 executor.shutdown()executor.shutdownNow(),避免线程资源泄漏。
  • 如果想和 Spring 的生命周期绑定,可以考虑用 ThreadPoolTaskExecutor(Spring 管理的线程池),并在容器销毁时调用 executor.destroy()

这样可以保证资源被安全回收。


7. 总结

本文解决的问题是:

  • 问题for 循环批量创建 DMLC 时,如果某个容器启动阻塞,会导致整个循环卡住。
  • 方案:使用线程池,将 DMLC 的启动过程放到异步线程中执行,避免主循环阻塞。
  • 原理:同步 → 异步的线程模型转变,把"坏容器"的阻塞控制在自己线程内,不影响其他容器。
  • 注意点:线程池需要在程序停止时销毁,避免资源泄漏。

最终效果是:即使部分 MQ 不可用,也不会影响其他 MQ 的监听容器正常启动,保证系统整体的健壮性。

相关推荐
陈随易2 小时前
适合中国宝宝的AI编程神器,文心快码
前端·后端·node.js
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 _基于SpringBoot技术的“树洞”心理咨询服务平台的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
阑梦清川2 小时前
AI编程实战记录贴2/100,关于Github提交代码失败的思考
后端
兮动人3 小时前
spring boot2升级boot3指南
后端
郭京京3 小时前
goweb模板语法html/template
后端·go
乐神嘎嘎嘎3 小时前
接口测试面试题
后端
AAA修煤气灶刘哥3 小时前
ES数据同步大乱斗:同步双写 vs MQ异步,谁才是王者?
分布式·后端·elasticsearch
Yvonne爱编码4 小时前
后端编程开发路径:从入门到精通的系统性探索
java·前端·后端·python·sql·go
bobz9655 小时前
ovn 厂商使用的规模
后端