Java微服务实战:RabbitMQ消息队列从业务设计到Docker部署与可靠性治理
发布时间:2026/9/26 13:03:48 作者:尧图编辑部 阅读量:1,286

做 Java 项目这些年消息队列几乎是躲不开的一环尤其是电商类的分布式系统。前段时间我完整做了一遍黑马商城这个项目的 RabbitMQ 模块从业务梳理、交换机设计到 Docker 部署、权限配置再到生产环境的可靠性治理整个过程踩了不少坑也把很多以前只停留在八股文层面的概念真正落到了代码和命令行里。这篇文章就把整个从开发到部署的过程完整拆一遍包含我实测过的配置、命令和排错思路不管是正在学微服务的同学还是工作中要上手 RabbitMQ 的开发者都能直接照着用。1. 项目先聊聊为什么黑马商城一定要上消息队列1.1 商城业务模型里的“重活”和“闲活”黑马商城本身是一套典型的电商微服务架构用户、商品、订单、购物车、支付、库存这些模块各自独立成服务。在没有消息队列之前服务之间的调用基本都是同步 RPC或者直接 HTTP 调用这样能跑通但是问题非常明显。下完单之后订单服务要调库存服务扣库存、要调积分服务发积分、要调通知服务发短信和邮件一个下单请求在订单服务里串联调用三条以上的服务链路接口耗时被拉得很长。我当时实际测过数据库里插入一条订单记录本身只要 20 毫秒左右但整个下单接口平均响应达到 800 多毫秒大部分时间都花在等待其他服务返回上。更尴尬的是如果积分服务或者通知服务挂了整个下单流程直接失败用户看到的是“下单失败”但其实订单根本没建出来。这就是典型的耦合问题。商城项目本质上有一批“必须立刻做的事”比如创建订单、锁定库存也有一批“可以稍后做的事”比如发送通知、记录日志、计算积分。前者要保证强一致和高可用后者完全可以异步化、缓冲化。引入消息队列就是把这些逻辑从同步链路里拆出去让核心链路轻下来也让下游服务有故障时主流程不至于全挂。1.2 三个消息队列的选型对比选型这块我纠结了挺久网上关于 Kafka、RabbitMQ、RocketMQ 的对比文章一抓一大把但大多数都是抄来抄去真正能指导决策的细节很少。说下我基于黑马商城业务场景的实际考量。Kafka 的核心优势在海量日志、吞吐量极高、追加写入的磁盘顺序 IO 非常猛但它不是一个严格意义上的企业级消息队列它的消费模型更适合离线处理和数据管道在复杂路由、延迟队列、死信处理这些场景下用起来很别扭。RocketMQ 是阿里开源的那套能力强事务消息做分布式事务确实香但前提是团队里有懂它运维的人它的控制台、Broker 部署、NameServer 集群维护成本比 RabbitMQ 高一截。RabbitMQ 走的是 AMQP 协议Erlang 写的路由模型非常灵活交换机、队列、绑定关系这套设计在业务系统里足够用。它的社区活跃度很高中小型电商系统每天几十万到几百万的消息量完全能抗住而且学习曲线平缓网上资料、面试题、排错经验都非常多。对于黑马商城这种教学性质浓、又要兼顾生产可用的项目RabbitMQ 是对团队最友好的选择。维度RabbitMQKafkaRocketMQ吞吐量万级/秒百万级/秒十万级/秒路由能力交换机绑定极强只能按 topic 分发有 tag 标签中等延迟消息/死信原生支持不支持事务消息强运维复杂度低中高高学习资料密度极高高较低1.3 为什么最终选择了 RabbitMQ选型不能只看技术名词要看项目整个生命周期。黑马商城最终选 RabbitMQ 有三个决定性因素。第一业务对路由的诉求非常强。订单超时关闭要发延迟消息下单完成要发通知支付成功要触发积分变更每种消息的消费方不一样路由策略也不一样。RabbitMQ 的交换机模型可以精确控制每个消息进入哪个队列尤其是 Topic 交换机可以用通配符按业务前缀路由这种灵活性是 Kafka 给不了的。第二部署成本低适合开发和部署环境。Docker 跑一个 rabbitmq:management 镜像前后端加控制台全齐了。Kafka 要 zookeeper 或 kraft 模式RocketMQ 要 NameServer 加 Broker 两个独立进程docker-compose 文件复杂度完全不是一个量级。第三面试和人才储备角度。Java 生态里 RabbitMQ 仍然是最常出现在简历和面试题里的消息队列大家都会一点团队接手成本低。这个项目的定位就是要让大家真正掌握一个队列而不是只会调 APIRabbitMQ 正好合适。2. 核心设计交换机、队列、路由规则这样搭2.1 业务场景梳理与消息分类动手写代码之前先把黑马商城需要用消息队列解决的场景全部列出来这一步非常重要。我梳理出来的核心场景有五个订单超时自动关闭这是最经典的一个。下单后如果 15 分钟内未支付订单要自动置为取消状态释放锁定的库存。这个场景不需要定时任务轮询数据库用 RabbitMQ 的延迟队列插件或者死信队列实现即可。下单成功后发通知包括短信、邮件、站内消息。这类通知对实时性要求不那么高即使延迟几秒用户也感知不到非常适合异步。支付成功后触发增值业务比如增加用户积分、更新会员等级、推送优惠券。这些服务依赖支付结果但是不希望在支付回调里串行等待。库存扣减后的日志异步落库把库存变更记录写入日志表同步做的话会拖慢核心事务。运营后台的报表数据汇总各个服务往队列里投递业务事件报表服务统一消费减少对业务库的直接查询压力。每个场景对应一套交换机、队列和绑定关系我在设计之初就用表格把关系定义清楚避免后面加到第五个队列的时候发现 routingKey 乱成一锅粥。2.2 交换机类型选择Direct、Topic、Fanout 怎么用RabbitMQ 的路由核心是交换机交换机有四种类型实际项目中我主要用了 Direct、Topic、Fanout 三种。Direct 交换机精确匹配 routingKey适合点对点通知类场景。如果支付成功后要发短信、发邮件、加积分我会定义三个绑定关系都用同一个 direct 交换机routingKey 分别是 sms、email、score生产者按需投递消费者只能收到自己绑定的消息。Topic 交换机支持通配符匹配* 匹配一个单词# 匹配零个或多个单词。我的订单相关消息统一走这种模式订单创建的消息 routingKey 定义为 order.created支付取消定义为 order.cancelled这样运营后台可以用 order.# 绑定整个订单域的所有消息而订单服务自己只绑定 order.created做业务隔离非常方便。Fanout 交换机最简单把所有消息广播到所有绑定的队列不关心 routingKey。黑马商城里我主要用在“商品数据变更同步”场景商品服务修改商品信息后广播一条商品变更消息搜索服务和缓存服务各维护一个队列同时收到消息然后各自刷新数据。2.3 Spring Boot 项目中的消息模型设计代码层面我用的是 Spring Boot 2.7 spring-boot-starter-amqp配置类里用 JavaConfig 声明交换机、队列和绑定关系。核心思路是把“业务消息定义”和“基础设施声明”分开交换机、队列这种基础设施集中在一个配置类里业务只面向 routingKey 编程。Configuration public class RabbitMQConfig { Bean public TopicExchange orderTopicExchange() { return new TopicExchange(mall.order.exchange, true, false); } Bean public Queue orderCreateQueue() { return new Queue(mall.order.create.queue, true); } Bean public Binding orderCreateBinding() { return BindingBuilder .bind(orderCreateQueue()) .to(orderTopicExchange()) .with(order.created); } }生产者发送消息时直接注入 RabbitTemplate指定交换机名和 routingKey。这个 subscribe 风格非常像我们日常开发里用的 rabbitTemplate.convertAndSend()但是要特别注意一个细节消息体序列化。默认的 SimpleMessageConverter 用的是 JDK 序列化会带上一堆类型描述头性能差而且跨语言不友好。我直接配置了一个 Jackson 转换器统一使用 JSON 格式。消费者用 RabbitListener 声明监听手动确认模式下要保证业务处理完成再确认消息。Component public class OrderCreateConsumer { RabbitListener(queues mall.order.create.queue) public void handle(OrderCreateMessage message, Channel channel, Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception { try { // 业务处理发送通知、记录日志等 channel.basicAck(tag, false); } catch (Exception e) { channel.basicNack(tag, false, true); } } }3. 坑最多的部署环节Docker 装 RabbitMQ 到权限验证3.1 镜像选择与推荐的启动方式黑马商城整套服务都跑在 Docker Compose 编排里RabbitMQ 的部署我建议直接用带管理界面的镜像。选择镜像时要看清 tag 后面是否带 managementrabbitmq:3.13-management 就是带控制台的如果你拉 rabbitmq:latest装完没有管理插件还要进容器手动开启 rabbitmq_management 插件特别麻烦。我第一次部署时就犯了这个错误docker 跑起来之后 5672 端口能连但是 15672 管理端口始终不通我还以为防火墙问题排查半天才发现是镜像没带 management 模块。后来调整了部署方式重新用管理镜像启动docker run -d \ --name rabbitmq \ -p 5672:5672 \ -p 15672:15672 \ -e RABBITMQ_DEFAULT_USERadmin \ -e RABBITMQ_DEFAULT_PASSadmin123 \ -v /data/rabbitmq:/var/lib/rabbitmq \ rabbitmq:3.13-management这里几个参数要解释一下。RABBITMQ_DEFAULT_USER 和 RABBITMQ_DEFAULT_PASS 是在容器首次启动时自动创建管理员账号比启动后手动 rabbitmqctl add_user 要省事得多而且这个账号天然带 administrator 标签可以直接访问管理界面。数据目录挂载到宿主机是必须的否则容器一删队列、消息、用户全部丢失尤其是交换机声明和排他队列这种动态数据持久化挂载能让你在升级镜像后不至于从零开始。3.2 admin 账号能登录为什么不能创建虚拟主机这是我在网上看到出现频率最高的一个问题自己也踩了一遍必须拿出来单独说。症状是管理界面能打开admin 账号也能登录但点击 Virtual Hosts 模块添加虚拟主机时创建按钮是灰的或者点击后没有任何反应后台日志会有 permission denied 之类的报错。这个坑的根源在于 Docker 镜像默认注册的 admin 用户权限范围。你通过 RABBITMQ_DEFAULT_USER 创建的用户虽然带有 administrator 标签但标签只是登录管理界面的资格管理虚拟主机还需要对根虚拟主机 / 有 configure 权限。如果 RabbitMQ 版本比较新启动脚本对默认用户的权限分配策略可能只给了监控权限而没有管理 vhost 的权限。解决方式有两种。一种是在容器内手动给用户授权docker exec -it rabbitmq rabbitmqctl set_permissions -p / admin .* .* .* docker exec -it rabbitmq rabbitmqctl set_user_tags admin administrator另一种是彻底点把整个 vhost 体系重新整理一遍。我给黑马商城的每个微服务都分配了一个独立的虚拟主机订单服务用 /order库存服务用 /stock而不是所有服务共用一个根 vhost。这样分配的好处是队列隔离、权限隔离后续做集群迁移也更干净。管理界面创建 vhost 时如果一直失败直接退回命令行去操作rabbitmqctl add_vhost /mall 比界面稳得多。3.3 rabbitmqctl 创建用户后管理界面连不上的问题另一类高频问题是 CLI 创建的用户登录管理界面时报“用户不存在或密码错误”或者干脆显示登录页面但一直跳不进去。这种问题大概率是用户标签缺失。CLI 创建的用户默认没有任何标签而管理界面默认只允许 administrator 或者 management 标签的用户登录。如果你用 rabbitmqctl add_user test test123 创建了用户不执行 set_user_tags 就跑去管理界面登录肯定会失败。我的习惯是在创建用户的命令后面立刻追加设置标签和权限的命令一条条执行不给自己留后门docker exec -it rabbitmq rabbitmqctl add_user dev dev123 docker exec -it rabbitmq rabbitmqctl set_user_tags dev management docker exec -it rabbitmq rabbitmqctl set_permissions -p /mall dev .* .* .*这里还要补一个容易忽略的知识点默认的 guest 用户只能在 localhost 地址访问如果你用镜像启动时没有指定默认账号而是想用默认的 guest 从远程访问一定会遇到 login refused。这也是我看到很多初学者卡住的地方建议直接用自定义管理员账号不要碰 guest。3.4 RabbitMQ 启动失败的常见排查路径部署过程中免不了遇到启动失败的情况我整理了一套自己的排查清单按顺序执行能解决绝大多数问题。第一步看日志。docker logs rabbitmq 前几百行就能看到 Erlang 虚拟机启动过程和插件加载情况最常见的启动失败原因是主机名解析不出来。RabbitMQ 内部会执行 hostname 解析如果容器 hostname 和 /etc/hosts 不一致Erlang 启动会报错。第二步检查端口冲突。5672 端口如果被本机其他服务占用RabbitMQ 起不来可以用 netstat -tlnp 快速确认。第三步检查配置文件权限。挂载了自定义 rabbitmq.conf 时经常因为文件权限问题导致服务读不到配置。虽然这个项目暂时不需要自定义 conf但最好知道这类问题存在。第四步确认插件状态。延迟消息插件 rabbitmq_delayed_message_exchange 需要单独安装启用如果队列声明时指定了 x-delayed-message 类型但插件没启用交换机创建会失败而且报错信息特别隐晦。docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_delayed_message_exchange这四条排查路径覆盖了我几个月踩坑记录的 90% 以上情况。4. 生产环境可用性消息不丢、幂等消费、失败重试4.1 消息不丢生产者确认和消费者手动 Ack分布式架构里最怕的不是写入慢而是消息丢了。RabbitMQ 消息丢失可能发生在三个阶段生产者发出但没到交换机、交换机路由不到队列、消费者拿到后没处理完就自动确认。三个阶段的防护手段完全不同我逐一说下黑马商城里的实际做法。生产者在发送消息时开启发送方确认模式spring.rabbitmq.publisher-confirm-typecorrelated这条配置开启后RabbitTemplate 发送消息会回调 ConfirmCallback只有当 Broker 返回 ack才能确定消息被交换机接收到。另外一个容易被忽略的是 ReturnsCallback如果消息不可路由会触发这个回调把消息退回两个回调配合才能完整覆盖前两个阶段。消费端更关键。Spring AMQP 默认是自动确认模式消息一送到消费者就自动 ack如果业务处理抛异常或者消费者进程被 kill消息就真的丢了。我的做法是开启手动 ack 模式业务处理成功才 basicAck处理失败 basicNack 并且重新入队或者进入死信队列。spring: rabbitmq: listener: simple: acknowledge-mode: manual retry: enabled: true max-attempts: 34.2 幂等消费唯一业务键去重手动 ack 能保证消息不因为异常丢失但重试机制会带来另一个隐患消息可能会被重复投递。消费者处理完消息后还没来得及 ack 就宕机了消息会重新入队再次被消费下游服务就会收到两条一模一样的业务请求。解决重复消费的标准做法是幂等设计。我在黑马商城里面维护了一张消息消费记录表核心字段是消息的唯一 ID业务操作之前先查这张表如果消费过直接跳过。消息的唯一 ID 在生产者创建消息时通过 MessagePostProcessor 设置到消息头里消费者第一件事就是提取这个 ID。RabbitListener(queues mall.order.create.queue) public void handle(Message message, Channel channel, Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception { String messageId message.getMessageProperties().getMessageId(); if (messageConsumeService.isConsumed(messageId)) { channel.basicAck(tag, false); return; } try { // 业务逻辑 messageConsumeService.markConsumed(messageId); channel.basicAck(tag, false); } catch (Exception e) { channel.basicNack(tag, false, false); } }这种方案比用 Redis setnx 做分布式锁要稳因为消息记录和业务操作在同一个本地事务里要么都成功要么都失败不会出现 Redis 标记成功但业务处理失败的情况。4.3 死信队列把重试不成功的消息隔离出来手动 nack 如果设置重回队列消息会不断被拉取、处理、失败、重回进入一个无限循环CPU 被打满而且问题永远暴露不到人工层。黑马商城里我设计了完整的死信队列机制。声明业务队列的时候给队列设置 x-dead-letter-exchange 和 x-dead-letter-routing-key这样业务处理重试超过最大次数仍然失败的消息不会回到原队列而是被转投到死信交换机然后进入死信队列。死信队列单独绑定一个消费者任务是对消息内容做记录存到失败日志表里再通过钉钉机器人发告警通知运维。这套体系上线后那些因为下游服务长期不恢复导致的消息堆积再也不会在业务队列里无限占坑排障只需要登录管理界面查看死信队列的 depth就能知道最近哪些消息出了大问题。4.4 集群部署与 Quorum Queue 仲裁队列单机部署只能用于开发环境生产上黑马商城至少需要三节点集群。RabbitMQ 集群有两种模式经典镜像队列和仲裁队列。镜像队列是老方案通过 policy 把所有节点的队列数据复制到多个节点读性能不错但主节点故障切换时可能存在脑裂。RabbitMQ 3.8 之后官方引入了 Quorum Queue基于 Raft 协议的仲裁队列数据强一致节点故障自动切换彻底消除了脑裂问题。实际部署中我把核心业务队列全部定义为仲裁队列声明时设置 x-queue-type 为 quorum。Bean public Queue orderCreateQueue() { MapString, Object args new HashMap(); args.put(x-queue-type, quorum); return new Queue(mall.order.create.queue, true, false, false, args); }这里要注意一个坑仲裁队列不支持常规的 exclusive 和自动删除属性也不支持某些非幂等操作如果老代码直接迁移会遇到不少异常但设计新项目时一步到位用 quorum 是最省心的选择。经典镜像队列的 policy 命令虽然配置起来简单我个人还是建议新项目直接用仲裁队列因为稳定性和一致性收益非常明显。5. 从黑马商城看分布式架构升级5.1 异步化之后的效果黑马商城接完 RabbitMQ 之后我专门做了压测对比。下单接口在改造前平均响应 800 多毫秒改造后订单服务只做本地事务、发一条消息接口响应降到 120 毫秒左右提升非常明显。这里面其实还藏了一个很多人不知道的细节订单服务发消息到 RabbitMQ 也是同步网络调用这几十毫秒仍然占用了接口时间如果对响应极度敏感可以在业务后用异步线程发消息或者参考 RocketMQ 的事务消息模式但 RabbitMQ 场景下没必要过度优化。同步调用的另一个问题链路中任何一个服务抖动整个接口就跟着抖动。异步化之后积分服务即使暂时不可用订单服务依然能正常返回消息暂时积压在队列里等服务恢复后再慢慢消费。这种削峰填谷的能力在促销场景尤其重要。想象一下秒杀活动瞬间涌入几千个订单如果没有消息队列缓冲同时去数据库写订单、扣库存数据库做不到的事太多了。5.2 延迟消息和分布式定时任务的联动订单超时关闭是电商系统的刚需黑马商城里我用 RabbitMQ 延迟队列插件实现。这里需要补充说明一下延迟消息的实现原理因为很多人只知道加一个插件不明白为什么能延迟。rabbitmq_delayed_message_exchange 插件创建的是一个延迟交换机消息发送到这种交换机时不会立即路由到队列而是先存在一个内部表中到期后才投递到目标交换机和队列。基于这个机制下单后往延迟交换机发一条消息设置 TTL 为 15 分钟15 分钟后消费者收到消息查数据库发现订单仍未支付就执行关闭动作。这套方案相比传统的定时任务轮询最大的优势是不用每秒钟扫描一次数据库的订单表减轻了数据库压力。但延迟消息插件本身是把消息存在内存或者 Mnesia 中大量堆积会占用内存生产上要注意延迟消息的量级。结合标题里的另一个热点Spring Cloud 架构中关于分布式定时任务的解决方案也值得一提。假如有一天你觉得 RabbitMQ 延迟消息不好用想回到定时任务方案普通的 Scheduled 只能单机执行多个实例会重复扫描。分布式场景下要么用 xxl-job 这类独立调度中心要么用 Redis 分布式锁配合 Scheduled确保同一时间只有一个节点执行任务。5.3 监控面板与日常运维经验RabbitMQ 管理界面提供了队列深度、消费速率、连接数这些核心指标日常排查我一般重点关注三个页面队列页面的 Ready 和 Unacked 数量、Channels 页面的连接状态、Overview 页面底部的 Erlang 进程和内存使用情况。消息堆积时Ready 数量会持续走高此时先确认消费者是否在正常运行再看消费逻辑是不是卡在了某个慢操作上。Unacked 数量高通常意味着消费者拿到消息后处理超级慢超时后又被重新投递这是典型的消费逻辑性能问题和消息量没有直接关系。内存告警也很常见。RabbitMQ 默认在内存使用率超过 40% 时开始阻止生产者继续发送消息这是初学者最容易误解的地方以为 RabbitMQ 卡了其实是在自我保护。遇到这种情况要看是不是消息积压导致内部队列占用了大量内存或者消费者处理速度跟不上而不是一味调高内存阈值。6. 拿这个项目去面试高频问题这样回答6.1 面试八股文里最常被问的几道题做完整套黑马商城的 RabbitMQ 模块面试时基本上所有消息队列相关的问题都能拿出实际项目细节去聊。我总结了几道高频题目和自己的答题思路。为什么使用消息队列这个问题一定有人问而且不能只说“解耦、异步、削峰”这三个词。要结合项目说比如下单接口原来串行调通知服务和积分服务耗时 800 毫秒改造后降到了 120 毫秒。用具体数字和真实业务场景佐证比背八股文有用得多面试官就是想看你是真正在项目里用过还是只是背了概念。如何保证消息不丢失这道题我的回答框架很固定生产者端用 confirm 模式保证消息到达交换机交换机配置持久化保证 Broker 不丢消息队列设置为持久化保证重启不丢消费者手动 ack 保证业务处理完才确认层层递进。消息重复消费怎么解决回答幂等设计展开讲那张消费记录表、唯一消息 ID、本地事务保证业务和消费记录同时落库。再补充一句即使没有重复消费业务服务本身也应该具备幂等能力因为网络重试在下游调用中非常普遍。消息堆积如何处理第一思路是提升消费者并发能力用 RabbitListener 配置 concurrency 参数增加并发消费者。如果消费者问题不在并发而在处理逻辑就要拆解消费链路把耗时操作改成异步。最后才会考虑临时增加新的队列和消费者集群来分摊压力。RabbitMQ 如何保证消息顺序这里要诚实一点RabbitMQ 默认不保证全局顺序只能保证单个队列内的顺序。如果业务一定要顺序消费方案是把需要保持顺序的消息通过同一个 routingKey 路由到同一个队列并且单线程消费或者给每条消息加有序序列号消费者拿到后先排序再处理。6.2 RabbitMQ 与其他队列的选型总结面试官很喜欢问“为什么用 RabbitMQ 不用 Kafka”这题的答案我在前面已经详细梳理过核心就三个点业务路由的灵活性、部署运维成本低、社区资料丰富。还要补充一点业务体量的考量黑马商城这种中小型电商系统每天百万级别的消息量RabbitMQ 完全够用Kafka 的高吞吐优势在这个体量下发挥不出来反而是多余的运维成本。如果面试官继续追问 Kafka 和 RocketMQ可以按吞吐量、路由能力、事务能力、运维成本四个维度做对比我通常背一张表出来比口头描述要清晰得多。最后加一句技术选型不是看谁最强而是看业务需要什么、团队能维护什么没有绝对的最好只有最合适的。6.3 项目复盘里最值得讲的三个细节面试官听你讲项目时最反感的就是只讲功能不讲难点。我在复盘黑马商城 RabbitMQ 模块时会主动抛出三个自己遇到并且解决掉的问题效果非常好。第一个是管理界面 admin 用户无法创建虚拟主机的问题描述了现象、排查过程、最终通过命令行 set_permissions 解决。这个细节能证明你真的部署过 RabbitMQ而不只是用 Docker 跑了个 Hello World。第二个是延迟消息插件不生效的问题原因是交换机类型声明错误或者插件没有启用讲了排查思路之后把验证插件状态的命令也带上。第三个是手动 ack 和死信队列的联动设计说明重试失败的消息如何被隔离、如何告警、如何人工介入这里可以体现你对可靠性设计的整体思考。结尾再分享几个我实际踩出来的经验最后说点个人体会。这个项目让我对 RabbitMQ 的理解发生了质的改变以前只知道发送接收现在能感受到消息队列在分布式系统里到底处在一个什么样的位置。做技术项目不能只停留在写代码和跑通功能的层面一定要亲手部署、亲手排障、亲手把一个看似简单的中间件调到可用状态。黑马商城这套 RabbitMQ 模块做完后我养成了几个工作习惯任何队列声明都先画清楚交换机、队列、绑定关系的表格任何消费者都必须写幂等逻辑任何部署都用 Docker Compose 而不是裸命令因为环境可复现才能让别人接手。建议你也把延迟消息、死信队列、仲裁队列这些特性逐个在本地环境验证一遍不要只在博客上看理论自己动手踩出来的坑面试的时候说起来才有底气。