RabbitMQ实战入门:从消息队列原理到黑马商城异步改造
如果你最近在跟黑马微服务课走到 Day10 大概率会在 RabbitMQ 这里停下来。前面刚用 Feign 把服务之间的接口调通转眼又要引入一个消息中间件很多人的第一反应是这东西到底解决什么问题我用 Feign 直接调不就行了我一开始也是这个疑问直到跟着黑马商城业务改造走了一遍才真正把 RabbitMQ 那套概念串起来。这篇文章就是 Day10 的完整总结既有原理层面的梳理也有从安装到代码再到业务改造的实操记录。包含本地部署 RabbitMQ 的两种方式、SpringBoot 整合步骤、消息转换器踩坑、以及黑马商城从同步调用改成异步通知的完整过程。适合正在学微服务、准备面试、或者想快速上手 RabbitMQ 的开发者参考。1. 微服务架构里为什么要引入消息队列1.1 没有 MQ 之前黑马商城的两个痛点先回到黑马商城这个项目本身。在黑马微服务课的前半段服务已经拆开了用户、商品、订单、库存、支付各自独立成服务。服务之间通过 Feign 做同步调用也就是服务 A 发 HTTP 请求等服务 B 返回结果后继续往下走。这种模式在业务简单时问题不大但黑马商城到了支付环节痛点就暴露出来了。支付服务在用户付完款之后不仅要更新自己的支付单状态还要通知订单服务把订单改成已支付通知库存服务扣减库存可能还要通知积分服务给用户加积分。如果全部用 Feign 同步调用一次支付成功需要连续调三个接口接口之间是串行等待。一旦库存服务或者积分服务出现慢查询、超时支付服务就必须一直等着。更麻烦的是这时候如果订单服务重启或者网络抖动支付服务重试几次都失败那订单状态就一直停在未支付用户付了钱却看不到订单更新这是很致命的问题。这就是同步架构的耦合问题一次业务操作的成功与否取决于所有下游服务的可用性。任何一个环节出问题整条链路都跟着出问题。1.2 消息队列带来的三个核心价值引入 RabbitMQ 之后上面的问题换了一种解决思路。支付服务不再直接调用订单、库存、积分服务而是把支付成功这个事件写进消息队列然后立刻返回。订单、库存、积分服务各自监听队列收到消息后各自处理自己的业务。这个架构变化带来三个非常实在的好处第一是异步。原来一次支付要等三个服务的结果可能要 500 毫秒。现在发一条消息到 MQ几十毫秒就返回了用户体验变好。这是消息队列最直观的价值。第二是解耦。支付服务完全不需要知道下游有哪些服务。将来再加一个消息通知服务或者发票服务支付服务的代码一行都不用改只要新服务去监听同一个队列或者同一个交换机就行。反过来某个下游服务挂了也不会影响支付主流程。第三是削峰。这个在秒杀场景尤其重要。黑马商城后面有秒杀活动某个瞬间大量下单请求打过来订单服务如果没有保护机制数据库连接池可能直接被冲垮。有了 MQ前端请求先打到订单服务订单服务把创建订单的消息快速写进队列后端消费者按照自己的处理能力慢慢消费等于给系统加了一个缓冲区。1.3 技术选型同样是消息队列为什么选 RabbitMQ市面上主流的消息中间件有 RabbitMQ、RocketMQ、Kafka 三款。为什么不选另外两个这是很多人学 Day10 之前会纠结的问题。简单说RabbitMQ 胜在轻量、功能完善、社区资料多特别适合中小型项目和入门学习。它的交换机模型后面会详细讲非常灵活能覆盖绝大多数业务场景而且基于 Erlang 开发天生具备高并发处理能力。RocketMQ 是阿里开源的产品功能也很强尤其是事务消息和延迟消息做得比 RabbitMQ 更顺手但部署和运维门槛更高适合已经在阿里云生态里的团队。Kafka 则偏向大数据场景吞吐量极高但功能上不如前两者丰富用在电商核心链路里反而有点大材小用。从学习角度看我的建议是先把 RabbitMQ 吃透。你理解了交换机、队列、路由键这套模型之后再学 RocketMQ 或者 Kafka会发现核心思路是相通的。技术选型本身没有绝对的对错关键看团队的技术栈、业务场景和运维能力。2. RabbitMQ 的核心概念一篇文章彻底理清2.1 五个核心角色生产者、消费者、交换机、队列、绑定RabbitMQ 这套模型刚接触时会觉得抽象因为它不像接口调用那样直观。我习惯用一个比喻来理解把 RabbitMQ 想象成一个快递中转站。生产者就是发件人消费者是收件人。队列就是快递货架消息先放到货架上消费者从货架上取。这里有一个关键角色叫交换机它的作用类似于快递分拣员。生产者不直接把消息放到队列里而是把消息交给交换机交换机再根据规则把消息投递到不同的货架。这就有个问题交换机凭什么知道消息该往哪个货架扔答案是绑定。开发者需要提前配置好这个交换机和那个队列绑定并且指定一个路由键。当生产者发送消息时带着一个路由键交换机拿消息的路由键和绑定时设置的路由键做匹配匹配规则由交换机的类型决定。这就是 RabbitMQ 和其他消息队列最大的不同它引入了交换机这个中间层。很多东西乍一看觉得多此一举但当你的业务需要一条消息发给多个消费者、或者按条件转发消息时交换机的价值就完全体现出来了。2.2 交换机四种类型的适用场景RabbitMQ 提供了四种交换机类型Day10 里面重点用到了前三种。第一种是 Direct 直连交换机。它按路由键精确匹配。比如队列 A 绑定的路由键是 order.pay队列 B 绑定的是 order.create生产者发消息时如果路由键写 order.pay就只有队列 A 能收到。这是最常用的一种适合一条消息发给指定队列的场景。第二种是 Fanout 扇形交换机。它不关心路由键收到消息后直接广播给所有绑定的队列。适合一对多广播场景比如用户下单成功后要同时通知库存服务、积分服务、短信服务三个服务各管各的队列用 Fanout 最省心。第三种是 Topic 主题交换机。它支持通配符匹配* 代表一个单词# 代表零个或多个单词。比如绑定键是 order.#能匹配 order.pay、order.create、order.cancel 等所有以 order. 开头的路由键。适合需要按主题做规则匹配的场景。第四种是 Headers 头交换机它不匹配路由键而是匹配消息的 headers 属性。实际项目里用得很少面试时知道有这个东西就行重点还是前三种。如果你只是把消息发到一个固定队列也可以用 RabbitMQ 默认提供的一个 Direct 交换机路由键写死队列名消息就会直接进指定队列。但这样做丢掉了交换机模型的灵活性项目里我不推荐太容易写死。2.3 持久化和确认机制先知道它们存在Day10 的代码能跑通之后很多初学者会忽略两个重要的概念持久化和消息确认。持久化解决的是 RabbitMQ 重启后消息还在不在的问题。默认情况下交换机、队列、消息都是不持久化的一旦 RabbitMQ 服务重启数据直接丢失。生产环境必须对交换机、队列、消息都开启持久化。消息持久化是在发送时设置消息的 deliveryMode 为 2SpringBoot 里可以通过 MessageProperties 来设置。消息确认解决的是消息是否真的被处理的问题。分为生产者确认和消费者确认。生产者发送消息后RabbitMQ 会返回一个确认消息告诉生产者我收到了。消费者处理完消息后也要给 RabbitMQ 回一个 Ack告诉它我处理完了你可以删掉这条消息了。这两个概念属于进阶内容入门阶段可以先有个印象真正写生产级代码的时候再深入。3. 本地环境搭建与 SpringBoot 整合3.1 Windows 安装 RabbitMQ 的完整流程Day10 的课基本是在 Windows 上从零开始的。这里先说最稳妥的安装流程再讲我踩过的坑。第一步是装 Erlang。RabbitMQ 是 Erlang 写的必须先装 Erlang 运行时。重点Erlang 版本必须和 RabbitMQ 版本匹配。我第一次装的时候随便下了一个最新版 Erlang结果 RabbitMQ 启动失败日志报了一堆看不懂的错误折腾了半天才发现是版本不兼容。建议去 RabbitMQ 官网的Erlang Version Compatibility页面查一下对应关系或者直接装 RabbitMQ 官方推荐的 Erlang 版本。第二步是下载 RabbitMQ 安装包。官网提供 Windows 安装版直接双击安装就行。安装完成后RabbitMQ 默认会注册成 Windows 服务在服务管理器里能看到 RabbitMQ 服务。第三步是启动管理界面插件。RabbitMQ 默认只开放 5672 端口给客户端连接管理界面需要单独开启。打开命令行进入 RabbitMQ 安装目录的 sbin 文件夹执行rabbitmq-plugins enable rabbitmq_management然后浏览器访问 http://localhost:15672用默认账号 guest / guest 登录。需要注意guest 用户默认只能在本机 localhost 登录如果你用远程 IP 访问管理界面会提示登录失败这是 RabbitMQ 的安全限制开发环境可以先不用管。3.2 启动失败排查两个最常见的原因我连着装了两台机器把 Windows 下 RabbitMQ 启动失败的情况碰了个遍这里直接说结论。第一类是 Erlang 版本不匹配。表现是安装完 RabbitMQ 后服务启动不了或者启动后马上停止事件查看器里能看到类似RabbitMQ: Erlang machine stopped instantly的错误。排查方法很简单去官网查版本兼容表卸载 Erlang装对应版本重启 RabbitMQ 服务。第二类是端口被占用。RabbitMQ 默认占用 5672 端口如果你之前装过其他消息中间件或者某个服务把 5672 占了RabbitMQ 服务也起不来。排查方法netstat -ano | findstr 5672如果有进程占用你可以把 RabbitMQ 配置文件里的端口改掉也可以找到占用进程并结束它。生产环境下建议换端口而不是强杀进程谁知道那个进程是干嘛的。管理界面打不开也可能是因为没启用插件或者 15672 端口被防火墙拦了。开发本机的话先检查插件有没有启用再检查防火墙。如果你不想折腾 Erlang 和安装包Docker 是更快的选择。3.3 Docker 部署 RabbitMQ一条命令起服务如果你本机装了 Docker部署 RabbitMQ 比 Windows 安装包省心太多。一条命令就能拉起来一个带管理界面的实例docker run -d --name rabbitmq \ -p 5672:5672 -p 15672:15672 \ -e RABBITMQ_DEFAULT_USERroot \ -e RABBITMQ_DEFAULT_PASS123456 \ rabbitmq:3-management这里重点解释几个参数。rabbitmq:3-management这个镜像已经内置了管理界面插件不需要再手动 enable。RABBITMQ_DEFAULT_USER和RABBITMQ_DEFAULT_PASS用来指定默认管理员账号省得登录后还要自己建用户。端口方面5672 是客户端连接端口15672 是管理界面端口。启动之后同样访问 http://localhost:15672用 root / 123456 登录。开发阶段用 Docker 是最舒服的不用管 Erlang 版本问题。但如果你是跟着黑马视频走视频里如果用的是 Windows 安装包我建议至少手动装一遍因为安装过程中暴露的问题、排查的思路本身就是学习的一部分。用 Docker 装完只是能用了不代表你理解了它。3.4 SpringBoot 生产者消费者代码环境准备好之后就开始写代码。SpringBoot 集成 RabbitMQ 非常顺手引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-amqp/artifactId /dependency配置文件spring: rabbitmq: host: localhost port: 5672 username: root password: 123456 virtual-host: /生产者发送消息是一个很简单的操作。注入 RabbitTemplate然后调用 convertAndSend 方法Autowired private RabbitTemplate rabbitTemplate; public void sendOrderMessage(String message) { rabbitTemplate.convertAndSend( hmall.direct, // 交换机名称 order.create, // 路由键 message // 消息内容 ); }消费者更简单在方法上标注RabbitListener就行Component public class OrderListener { RabbitListener(queues order.queue) public void listenOrderQueue(String message) { System.out.println(收到订单消息 message); // 在这里处理业务逻辑 } }这里有个很多人一开始会犯的错误消息发到交换机之后如果交换机上没有任何队列绑定路由键或者消费者监听的队列名对不上消息就会丢失而且不报任何错误。排查的时候光看日志是看不出来的要到管理界面的 Queues 和 Exchanges 里查看绑定关系是否正确。3.5 消息转换器默认序列化方式是个坑跟着课程写代码的同学大概率会在消息转换器这里卡一下。SpringBoot 默认的消息转换器是SimpleMessageConverter它用 JDK 自带的序列化机制。如果生产者发送的是一个对象默认会把对象序列化成二进制字节流。消费者接收到之后会尝试用 JDK 反序列化还原对象。问题在于如果生产者和消费者属于不同的服务而且这两个服务里都没有一个完全相同的类包名、类名、字段都要一致反序列化就会失败直接抛 ClassNotFoundException 或者类型转换异常。黑马商城里生产者在订单服务、消费者在库存服务两个服务各自的实体类并不完全一致这就会踩坑。解决方案是配置一个 JSON 消息转换器让消息以 JSON 格式传输。在配置类中定义一个 BeanConfiguration public class RabbitMQConfig { Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }配置之后生产者发送对象时RabbitTemplate 会自动把对象转成 JSON 字符串消费者接收时RabbitListener方法的参数上如果写的是具体类会自动从 JSON 反序列化。这就把生产者和消费者的实体类解耦了两边各写各的类只要字段能对上就能正常转换。从这之后我建议所有项目都统一配置 JSON 消息转换器别用默认的序列化方式。4. 黑马商城业务改造实战记录4.1 改造目标把支付回调从同步轮询变成异步通知黑马商城业务改造这是 Day10 的重头戏。课程里给了一个非常典型的场景原本用户在支付成功后支付服务会通过 Feign 直接调用订单服务把订单状态改成已支付。这个流程有个隐患如果订单服务响应慢支付服务的线程会被拖住如果订单服务挂了整个支付流程的直接结果就不可控。而且按照后面的业务发展支付成功后不止订单服务需要感知库存服务要扣库存积分服务要加积分通知服务要发消息这种一对多的通知用 Feign 写起来就是灾难。改造的思路是支付服务不再关心支付成功之后还有谁关心这件事它只负责往交换机发一条支付成功的消息。订单服务、库存服务、积分服务各自监听自己的队列收到消息后并行处理。这就把原来支付服务和多个下游服务的串行同步调用变成了生产者与消费者解耦的异步通知模型。4.2 交换机、队列和绑定关系的设计改造前首先要规划好交换机和队列。黑马商城这里我用了一个 Topic 交换机也可以拆成多个 Direct 交换机看个人习惯。我的设计方案如下交换机名称hmall.topic队列order.pay.queue订单服务监听处理订单状态更新stock.deduct.queue库存服务监听处理扣减库存point.add.queue积分服务监听处理加积分绑定关系order.pay.queue绑定键order.paystock.deduct.queue绑定键stock.deductpoint.add.queue绑定键point.add这样设计的好处是路由键语义清晰。支付服务发消息时路由键写order.pay消息只会进订单队列如果想让消息同时触发扣库存和加积分就又发两条消息路由键分别写stock.deduct和point.add。将来新增消费者只需要新绑定一个队列到交换机生产者的代码一行都不用改。在 SpringBoot 里队列、交换机、绑定关系推荐用配置类声明成 Bean应用启动的时候 RabbitMQ 会自动创建这些资源Configuration public class RabbitMQConfig { Bean public TopicExchange topicExchange() { return new TopicExchange(hmall.topic, true, false); } Bean public Queue orderPayQueue() { return new Queue(order.pay.queue, true); } Bean public Binding orderPayBinding() { return BindingBuilder .bind(orderPayQueue()) .to(topicExchange()) .with(order.pay); } // stock.deduct.queue 和 point.add.queue 的声明方式一致略 }构造方法里第二个参数true表示队列持久化生产环境必须写true。4.3 生产者改造支付服务发消息支付服务在支付成功后不再调用 Feign 接口而是通过 RabbitTemplate 发送消息。Autowired private RabbitTemplate rabbitTemplate; public void paySuccess(PaySuccessDTO paySuccessDTO) { // 更新本地支付单状态 updatePayStatus(paySuccessDTO.getOrderId(), PayStatus.PAID); // 发送订单已支付消息 rabbitTemplate.convertAndSend( hmall.topic, order.pay, paySuccessDTO ); // 发送扣减库存消息 rabbitTemplate.convertAndSend( hmall.topic, stock.deduct, paySuccessDTO ); // 发送加积分消息 rabbitTemplate.convertAndSend( hmall.topic, point.add, paySuccessDTO ); }这里要注意如果有多个消费场景不要在一条消息里塞多个业务含义尽量做到一条消息对应一个业务动作。order.pay、stock.deduct、point.add这三个路由键虽然数据内容差不多但在语义上是三个独立的业务事件分开发送更清晰后面排查问题也好查。4.4 消费者改造订单、库存、积分服务各自监听订单服务这边监听order.pay.queue收到消息后把订单状态改成已支付Component public class OrderPayListener { Autowired private OrderService orderService; RabbitListener(queues order.pay.queue) public void handlePaySuccess(PaySuccessDTO paySuccessDTO) { orderService.updateOrderStatus(paySuccessDTO.getOrderId(), OrderStatus.PAID); } }库存服务和积分服务的代码结构完全一样只是队列名和业务逻辑不同。这就是 MQ 解耦带来的好处每个服务只关心自己的队列各改各的代码互不影响。4.5 改造后的完整调用链路把整条链路串起来看改造前后的对比非常明显。改造前 用户点击支付 - 支付服务调订单服务等待返回- 调库存服务等待返回- 调积分服务等待返回- 返回支付成功任何一个环节超时用户就要一直等甚至看到报错。改造后 用户点击支付 - 支付服务更新支付单 - 发三条 MQ 消息 - 立即返回支付成功订单服务和库存服务在后台异步处理互不干扰。哪怕库存服务此刻正在处理大量请求订单服务也不会被拖住。用户感知到的响应时间大幅缩短系统的整体吞吐量也上去了。改造完之后可以做一个简单的验证启动消费者服务到管理界面手动发一条消息或者正常走一遍下单支付流程观察消费者日志里是否能收到消息、队列里的消息数量是否正确消耗。我用 RabbitMQ 管理界面看过状态之后对整个消息流转过程就有了更直观的感受。5. 踩坑实录与面试高频问题速查5.1 常见问题排查记录这部分我直接整理成一个速查表都是实操中容易遇到的问题方便你对照排查。问题现象可能原因排查与解决连接 RabbitMQ 时报 Connection refusedRabbitMQ 服务没启动 / 端口不对 / 防火墙拦截检查服务状态确认 5672 端口可访问登录管理界面返回 login failedguest 用户只能本机登录 / 账号密码错误本地用 guest / guest远程需创建新用户并授权消费者收不到消息交换机类型或路由键不匹配 / 队列没绑定正确 / 消费者监听了错误的队列到管理界面 Exchanges、Queues 页面核对绑定关系发送消息报 ERR 404交换机名称拼写错误 / 交换机未声明检查交换机名称确认已声明并持久化消费者收到消息后类型转换报错默认 JDK 序列化类名不一致配置 Jackson2JsonMessageConverterRabbitMQ 服务启动后立即停止Erlang 版本不兼容 / 端口被占用查版本兼容表用 netstat 排查端口占用消息丢失且无报错交换机没有匹配的队列 / 消息未持久化检查绑定关系生产环境开启持久化这些坑我基本全踩过一遍。最耗时间的还是消费者收不到消息的问题因为它不报错单纯看代码很难发现。我的经验是遇到这类问题不要瞎猜直接打开管理界面在 Exchanges 点进交换机能看到它的所有绑定队列和绑定键然后核对生产者发送的路由键一两分钟就能定位问题。5.2 消息可靠性与幂等设计跟着课程把代码跑通之后如果你想更进一步建议考虑两个生产环境绕不开的问题消息可靠性消息不丢和幂等性消息重复处理。消息不丢失需要三个层面协同生产者确认确认消息已到 MQ、交换机/队列/消息持久化防止 MQ 重启丢数据、消费者手动 Ack确认消息已处理完成。RabbitMQ 的发布确认机制核心代码是开启publisher-confirm-type: correlated然后在发送消息时获取确认回调。消费者手动 Ack 则是把确认模式改为手动处理完业务逻辑后再调用channel.basicAck。消息重复消费解决思路是幂等。因为网络抖动、消费者处理超时等原因MQ 可能把同一条消息投递两次。比如订单支付成功的消息如果被消费两次订单状态可能被更新两次虽然最终结果一样但如果第一次更新状态前代码里判断逻辑不严谨就会出问题。黑马商城里的做法就是基于订单状态做判断如果订单已经处于已支付状态重复消息直接跳过。更通用的做法是在业务表里建唯一索引或者用 Redis 做去重记录。5.3 RabbitMQ 与 RocketMQ 选型对比面试或者做技术选型的时候经常会被问到 RabbitMQ 和 RocketMQ 的区别。我整理了一个核心对比表对比项RabbitMQRocketMQ开发语言ErlangJava协议支持AMQP、MQTT、STOMP自定义协议吞吐量中等单机万级较高单机十万级事务消息支持程度一般原生支持较完善延迟消息需通过插件或死信实现原生支持社区资料非常丰富老牌产品阿里生态国内资料较多运维复杂度较低较高如果是中小型团队、业务场景没那么复杂RabbitMQ 完全够用。如果对事务消息、延迟消息有强需求并且团队 Java 技术栈成熟RocketMQ 更合适。面试时不用背一堆指标说清楚选型依据比罗列数字更有说服力。5.4 面试常问的几个点最后总结几个高频面试题你在学习的时候可以带着问题去理解RabbitMQ 如何保证消息不丢失从生产者确认、持久化、消费者 Ack 三个角度来说。如何保证消息不被重复消费答幂等设计结合数据库唯一键或者业务状态判断。RabbitMQ 有哪些交换机类型Direct、Fanout、Topic、Headers重点说前三个的匹配规则。为什么不能用同步 Feign 调用代替 MQ从同步等待、耦合度、削峰三个角度对比。消费积压怎么办增加消费者数量、提高消费并发度或者紧急情况下临时转型数据。这些问题在 Day10 内容里都能找到答案的基础。当你真正亲手改完黑马商城的业务再回头看这些面试题会发现它们不再是死记硬背的八股文而是你实际操作中本来就遇到过的问题。我个人在实际操作中的体会是RabbitMQ 入门最大的门槛不是代码而是思维转换。你习惯了 Feign 那种你给我请求我还你响应的同步模式之后突然要接受我把消息扔进队列就不管了这种异步模式会很不适应总担心消息丢了怎么办、消费者是不是真的收到了。这种担心很正常解决办法就是通过管理界面观察消息流转把每一个概念和界面上看到的现象对应起来。等你想明白交换机只负责路由不负责存储队列存储消息消费者处理消息这句话RabbitMQ 就算真正入门了。接下来再去补事务消息、死信队列、延迟队列这些进阶内容就会顺畅很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →