
XXL-MQ 这个名字我最早是在看 XXL 系列开源项目时注意到的。作为一套同样走轻量级路线的分布式消息队列它强调的不是把 Kafka 那些重型能力全部搬回家而是用很克制的依赖和配置把生产消费链条里最常见的那几件事做扎实可靠投递、灵活路由、消息管理、并发消费。如果你正在做中小规模的微服务拆解或者只想在 Spring Boot 项目里塞一个不折腾的消息中间件这份使用手册应该能帮你少踩不少坑。这套东西适合谁我的判断是三类人一是被 Kafka 和 RabbitMQ 的架构复杂度劝退的小团队二是需要在单机或两三台机器内解决问题、又不想引入额外运维负担的开发者三是已经用过 XXL-JOB、对 XXL 系列风格比较熟悉、希望用同一套思维管理消息任务的工程师。我会从部署、核心 API、高级特性再到线上排查把 XXL-MQ 的用法完整过一遍。1. 项目到底是什么XXL-MQ 的定位与适用场景1.1 轻量级不等于功能缩水而是把资源用在刀刃上传统的“重量级”消息队列比如 Kafka光看它的架构组件就够人消化一阵broker、controller、zookeeper 或 KRaft、分区、副本、ISR……每个概念背后都是一套复杂的分布式协调逻辑。XXL-MQ 的路线完全相反它把消息的存储放在后端关系型数据库把网络通信交给 Netty把消费调度做成任务式轮询这样就把“分布式”的门槛从“我要维护一个集群”降到了“我只需要一个数据库和一个进程”。我实际部署时的体会是XXL-MQ 的定位更像是给中小团队准备的“精简版 RabbitMQ”——它只保留能覆盖 90% 业务场景的能力点对点队列、发布订阅、延迟消息、消息重试、消费确认。像 Kafka 那样的日志堆积、分区顺序保证在大部分业务项目里根本用不到强行引入只会增加排障难度。资源占用方面一个 XXL-MQ broker 节点最小只需要 256MB 内存就能跑起来这在云上小规格 ECS 或内网测试环境里非常友好。1.2 三种典型使用场景与不适合的场景我看到的真实落地场景主要有三种。第一种是订单系统的异步化。用户下单后把订单创建事件发送到 XXL-MQ积分服务、短信服务、库存服务各自订阅消费互不等待整体接口耗时能从 800ms 降到 120ms 左右。第二种是定时任务结果收集。配合 XXL-JOB 跑完批处理任务后把结果摘要发给消息队列由另外的服务生成报表避免任务模块越做越重。第三种是应用解耦的过渡方案——当你还没能力维护一个专业 MQ 集群时先用 XXL-MQ 把生产者和消费者的调用链断开等业务量真的涨到需要 Kafka 的时候再替换。不适合的场景同样要讲清楚如果你需要分钟级以上的消息堆积或者单日消息量过亿又或者必须保证严格的全局顺序建议直接上 Kafka 或 Pulsar。XXL-MQ 的存储模型基于数据库消息量一上来数据库的读写压力会先吃不消。我的经验是三千万条以下、单条消息不超过 1MB 的场景它都表现得很靠谱再往上你就该考虑迁移了。2. 十五分钟跑通部署与基础配置2.1 环境准备与安装步骤先列环境要求JDK 8推荐 JDK 8 或 11、MySQL 5.7也可用 MariaDB、Maven 3.6。XXL-MQ 的部署分成两部分一个是 broker-server负责消息收发和存储调度一个是 admin 控制台提供可视化管理界面。从 GitHub 下载源码包后进入项目目录执行构建cd xxl-mq-2.3.1 mvn clean package -DskipTests构建完成后在xxl-mq-admin/src/main/resources/下找到db.sql把它导入到 MySQL。这一步是很多新手会忽略的XXL-MQ 不像 Kafka 那样把消息存到本地磁盘文件它把消息主体、消费进度、重试记录全部落到数据库表里所以数据库必须提前建好。导入完成后建议顺手确认一下三张核心表——xxl_mq_message、xxl_mq_consumer_progress、xxl_mq_retry_queue——是否创建成功后面排查问题时这三张表是最常打交道的。2.2 配置文件里最容易改错的三个地方最常被问到的是配置文件。broker 和 admin 各自有一个application.properties关键项我列一下# broker server.port9999 xxl.mq.broker.registry.mysql.urljdbc:mysql://192.168.1.20:3306/xxl_mq?useSSLfalsecharacterEncodingUTF-8 xxl.mq.broker.registry.mysql.usernameroot xxl.mq.broker.registry.mysql.password123456 xxl.mq.broker.channel.port9998 # admin server.port8080 xxl.mq.admin.usernameadmin xxl.mq.admin.passwordadmin123第一个易错点server.port和channel.port是两个概念。server.port是 broker 提供给客户端 SDK 长连接的通信端口channel.port是 broker 节点间内部通信用的端口配置成同一个端口会导致启动冲突。第二个易错点数据库连接串里的characterEncodingUTF-8千万别省否则中文消息体在部分 MySQL 库里会变成乱码。第三个易错点admin 的登录账号默认是admin密码123456第一次启动后要立刻改掉因为 admin 控制台可以查看所有消息内容。注意生产环境不要只改端口和密码还要检查 broker 的注册中心地址是否使用了内网 IP。如果用127.0.0.1注册其他机器上的生产者将连不上 broker而你在本机测试时却一切正常这种问题非常隐蔽。启动顺序上先启动 broker再启动 admin。等 broker 日志出现XxlMqBroker started后再打开http://localhost:8080就可以看到控制台了。我记得自己第一次部署时把顺序反过来admin 一直注册不上 broker日志里报connection refused这个问题排查了很久后来才发现是启动顺序导致的。3. 手把手掌握核心 API 与消息模型拆解3.1 三种消息模型点对点、发布订阅与广播XXL-MQ 支持三种消息模型用在不同的业务场景。点对点P2P一条消息只会被一个消费者消费。适合任务分发、异步处理。发布订阅Pub/Sub同一条消息会被同一个消费组内的一个消费者消费不同消费组各自独立每个组都能收到消息。适合事件广播、数据同步通知。广播Broadcast无论是否属于同一消费组所有在线消费者都会收到消息。适合配置刷新、本地缓存失效通知。点对点和发布订阅的区别我习惯用一个生活例子解释点对点像给一个同事发私聊群里其他人看不到发布订阅像在公司群里发通知每个部门消费组只需要一个代表回复广播则是全公司所有人群发消息每个人都会收到。理解了这个写消费者代码时就不容易把消息模型弄混也不会出现“明明广播模式下我只想消费一次”这种预期错位。3.2 生产者 API同步发送与异步发送引入依赖后生产者的代码非常简单。先创建一个实例XxlMqProducer producer new XxlMqProducer(xxl-mq-producer-group, 127.0.0.1:9999); producer.start();然后发送消息。我一般会用异步发送因为它不阻塞主线程吞吐更高producer.asyncSend(order.topic, order-created, {\orderId\: 1001}, 3000);这里四个参数分别是 topic、业务关键词 tag、消息体、超时时间毫秒。最后一个参数容易被忽视但 3 秒是一个比较稳的默认值如果 broker 负载较高可以放宽到 5 秒。同步发送则返回MessageSendResult里面带有messageId这个 ID 是后面排查消息去向的关键也是控制台里检索消息的主键。我踩过的一个坑是连接 broker 的地址写成了 admin 的 8080 端口。SDK 通信走的是 broker 的server.port9999跟 admin 的 HTTP 端口完全不是一回事。如果你发现asyncSend没有回调、控制台也查不到消息先检查这个地址。另一个小建议生产者的producerGroup不要起得太随意它会出现在管理控制台的统计中如果用一堆test1、test2之类的名字后续监控根本分不清是哪个服务在发消息。3.3 消费者 API注解式监听与手动 ACK消费者端XXL-MQ 最受欢迎的是注解式监听。在 Spring 项目中只需要注册一个 Bean不需要手动管理连接生命周期框架会在应用启动时自动订阅Component public class OrderMessageListener { XxlMqListener(topic order.topic, consumerGroup order-group, model ConsumeModel.CLUSTER) public void onOrderCreated(String message) { Order order JSON.parseObject(message, Order.class); orderService.handleOrderCreated(order); } }这里model ConsumeModel.CLUSTER表示集群模式同一个消费组内只有一台机器消费这条消息改成BROADCAST就是广播模式。默认情况下监听方法执行完、没有抛异常XXL-MQ 就会自动向 broker 提交 ACK。如果方法抛了异常消息会进入重试队列按照你配置的重试策略进行延迟重投。如果想要更细粒度的控制可以用手动 ACKXxlMqListener(topic order.topic, consumerGroup order-group) public void onOrderCreated(MessageContext context, String message) { try { // 业务处理 context.ack(); } catch (Exception e) { context.reject(); // 触发重试 } }手动 ACK 适合那种“任务必须确认落库才能算完成”的场景。我一般只有在跨服务限流、需要精确记录消费状态时才会用手动 ACK其余场景全部交给自动 ACK少写代码也少一些出错的机会。4. 高级玩法延迟消息、优先消息与事务消息4.1 延迟消息用一张表解决定时任务解耦延迟消息是电商场景的刚需最常见的例子是“下单后 30 分钟未支付自动关闭订单”。如果在业务代码里写定时轮询会有两套问题一是轮询频率和实时性的矛盾二是多实例部署时会重复扫描导致误关单。XXL-MQ 内置延迟消息发送时指定延迟级别即可producer.delaySend(order.topic, order-close-check, {\orderId\:1001}, 30 * 60 * 1000L);底层实现我不展开源码但原理上是通过一张延迟消息表记录消息的投递时间后台线程到点后把消息流转到正式队列。使用上有一个注意事项延迟消息的触发精度不是毫秒级别后台扫描周期默认是 1 秒所以需要精确到 200ms 以内的场景不太适合。还有一个容易被忽略的点延迟消息如果设置了过多不同的延迟时间后台扫描 SQL 的过滤条件会变复杂性能会受影响建议把延迟时间收敛到少数几个固定档位比如 10s、30s、5m、30m、1h、24h。4.2 事务消息先发后提交的落地姿势分布式事务是消息队列逃不开的话题。XXL-MQ 的事务消息走的是业界标准的“两阶段”流程先发送 half 消息不立即投递给消费者本地事务执行成功后再 commit 这个 half 消息此时消费者才会真正看到消息如果本地事务失败就 rollback。代码大概是TransactionMessageHolder holder producer.beginTransactionMessage(); try { producer.sendHalf(order.topic, order-created, {\orderId\:1001}); orderMapper.insert(order); // 本地事务 producer.commitTransactionMessage(holder); } catch (Exception e) { producer.rollbackTransactionMessage(holder); }事务消息的实用性在于它让消息发送和本地数据库操作处于“要么都成功、要么都回滚”的语义中。但实际使用中我建议大家尽量少写事务消息因为链路长一个节点出问题排查起来非常痛苦。我的做法是优先把业务做成幂等消费者端根据messageId去重配合延迟重试比事务消息简单且可靠。事务消息只在“必须保证消息不丢失且严格在业务成功后可见”的核心链路上用比如支付结果通知。4.3 优先级消息与消息过滤优先级消息在 XXL-MQ 里不是通过额外物理通道实现的而是在发送时给消息打上优先级标记broker 在消费调度时会优先投递高优先级消息。我见过不少团队把所有消息都标成高优先级结果普通消息长期无法消费队列的公平性被破坏。我的配置经验是高优先级消息占比不要超过总量的 20%否则优先级机制反而成了故障放大器。消息过滤则依托于 topic 下的 tag。发送时producer.send(order.topic, pay-success, body)tag 是第二个参数消费者监听时用tag pay-success限定只接收该 tag。这种方式比在消费端手动 if 判断要好因为过滤发生在 broker 端网络传输量和消费端 CPU 开销都会小很多。我见过有些团队把不下三十个业务事件塞进同一个 topic又没有用 tag 区分消费者得自己做一大串类型判断代码维护成本直线上升——建议一个 topic 最多五到八个 tag和“一个表不要有太多列”是同一个道理。5. 控制台、监控调优与常见坑位排查5.1 Web 控制台能做的事消息检索、消费进度与重发XXL-MQ 的 admin 控制台是我觉得它比很多轻量级 MQ 好用一大截的地方。打开控制台左侧菜单能看到“消息查询”、“消费进度”、“堆积告警”、“死信队列”几块。消息查询支持按messageId、topic、发送时间范围检索点进去能看到完整的消息体、发送 IP、消费状态、ack 时间。这个功能在联调阶段是救命级的以前用别的 MQ 排查消息丢失得翻日志、抓包在 XXL-MQ 里直接控制台就能定位到是哪一步断的。消费进度模块展示了每个消费组在每个 topic 上已经消费了多少条、还有多少条积压“待消费”数字就是最直接的告警指标。死信队列更关键——消息重试超过最大次数后不会直接丢掉而是进入死信 topic可以在控制台里手动重发或降级处理。我碰到过几次消费者因一个数据格式问题持续消费失败的场景如果没有死信队列消息就默默地消失了事后审计完全无据可查。5.2 性能调优线程数、批量拉取与数据库连接池XXL-MQ 调优的核心是 broker 端的三个参数参数默认值我的建议channelHandlerThreads64根据 CPU 核数调整8 核机器建议 128messageBatchSize100消费端拉取时一次拉取的消息条数大流量可调到 200retryQueueScanInterval3000ms延迟重试扫描周期一般不用改消费者端的线程池也有讲究。XxlMqListener默认使用一个共享线程池如果多个 topic 的消费者处理逻辑都比较重会出现互相挤占的情况。我给的建议是为耗时超过 500ms 的监听器单独配置线程池Bean(orderListenExecutor) public ThreadPoolTaskExecutor orderListenExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(200); return executor; }传参时用XxlMqListener(topic order.topic, executor orderListenExecutor)。如果多个监听器共用一个线程池命名规范也很重要建议把线程池名称和 topic 对应起来比如orderListenExecutor、stockListenExecutor这样线上用 arthas 排查线程栈时一眼就能看出哪个业务线程卡住了。还有一个容易被忽视的数据库连接池参数broker 的数据库连接池最小连接数不能太小因为每一条消息的写入、ack 更新都要占用数据库连接默认 10 经常不够建议改成 20~30否则流量一上来线程会大量阻塞在等待连接上。5.3 高频问题排查速查表与实操心得最后整理一下我实际运维中遇到过的高频问题做成速查表现象排查方向解决方案消费者一直收不到消息broker 端口是否可达topic 名是否一致tag 是否匹配先用控制台按 topic 查消息看消息状态是否为“已投递”部分消息重复消费消费者没有做幂等ack 超时触发重投消费端用messageId做去重去重表加唯一索引消息堆积一直降不下来生产端速率过高数据库连接池打满调大messageBatchSize检查慢 SQL给消息表加索引重启后消息不见可能配置了非持久化存储检查 broker 的存储模式生产环境必须开启持久化延迟消息不触发延迟时间超出最大支持档位扫描线程被阻塞查看延迟时间是否超限检查后台线程日志再补一个我个人运营中的习惯XXL-MQ 上线后的前两周建议把 admin 控制台的消费进度截图或者导出一次留作基线。因为一旦流量起来后再排查积压“现在积压了 5 万条”这个数字如果没有基线对比你根本无法判断是突增还是长期累积。有了基线你能很快算出积压速度定位到是生产突发还是消费能力下降。6. 由单机到集群多节点部署与高可用取舍6.1 集群模式下的注册、连接与消息写入很多人在“分布式消息队列”这几个字里默认了“集群部署很复杂”其实 XXL-MQ 的多节点部署比想象中要直白。多个 broker 节点共用同一个 MySQL 注册中心启动时把自己的 IP 和端口注册进去客户端 SDK 从注册中心拿到 broker 地址列表后可以配置轮询或随机策略建立长连接。关键点在于集群模式下消息并没有像 Kafka 那样按分区散列到不同节点而是每个 broker 节点都能独立接收消息并把消息写入同一个库。这样做的好处是节点无状态水平扩容时不需要做数据迁移代价则是数据库会成为集中依赖所以集群方案里数据库一定要单独部署不能用 broker 本机自带的库。生产上我一般建议两节点起步一主一备加主从数据库足够覆盖绝大多数可用性要求。客户端配置可以写成多个节点地址XxlMqProducer producer new XxlMqProducer(producer-group, 192.168.1.10:9999,192.168.1.11:9999);6.2 高可用设计的核心存储层才是真正的瓶颈既然多个 broker 都往同一个数据库写那么集群高可用的成败就落在了存储层上。MySQL 主从同步如果延迟大消息的消费进度和写入记录可能出现不一致好在 XXL-MQ 的消息表、消费进度表都有主键和唯一索引客户端连接切换后仍能以messageId为准做幂等不会产生重复投递的灾难。另一个要做的是客户端断线重连。broker 节点宕机后SDK 会在几秒内感知并切换连接。这个“感知”依赖心跳参数建议把心跳间隔调成 3000ms失败重试次数调成 3 次这样单节点故障对业务的影响能控制在 10 秒以内。不过我还是要泼一点冷水如果你的团队连 MySQL 主从都没有精力维护那老老实实跑单节点就好轻量级 MQ 的“轻”本来就是用牺牲一部分高可用换来的刻意加节点反而会放大运维风险。说实话从第一次接触这个轻量级分布式消息队列到现在我觉得它最大的价值不是“能用”而是“看得见”。消息进没进来、消费到哪里了、哪条消息出了问题控制台把整个链路摊开在面前这对运维自信心非常重要。你不需要背 API不需要猜内部行为打开界面就能回答业务方的灵魂拷问我的消息到底发了没有如果你也在找一个不折腾、能快速落地、又能清楚看到全链路状态的消息队列按这套手册走一遍大概率会有一个比较顺的整体体验。