一、消息中间件基础
1. 为什么要用消息队列
核心解决三大业务痛点,这也是消息队列最核心的价值:
- 异步处理 :把非核心、耗时长的操作交给 MQ 异步执行,不用让用户同步等待。
- 生产例子:用户注册成功后,需要发送欢迎邮件、短信通知。这些操作不影响注册结果,把通知消息丢进 MQ 后,接口直接返回注册成功,大幅降低接口响应时间。
- 服务解耦 :服务之间不用直接调用,通过 MQ 传递消息,修改任一服务都不会影响其他服务。
- 生产例子:下单系统原本要直接调用库存、物流、积分接口,耦合度很高。现在下单成功后只发一条消息到 MQ,各个服务自己订阅消费;后续新增营销服务时,只要自己订阅消息即可,完全不用修改下单系统代码。
- 流量削峰 :把高峰期的突发流量先暂存到 MQ 里,系统按自身处理能力慢慢消费,避免被瞬时流量冲垮。
- 生产例子:商品秒杀活动,瞬间涌入几万下单请求,数据库只能扛住每秒 1000 次写入。请求先全部进入 MQ,系统按每秒 1000 条的速度消费写库,平稳度过流量高峰。
2. 什么是消息中间件
MQ(Message Queue)本质是先进先出的消息队列,程序之间通过发送消息进行通信,不需要直接互相调用。就像生活中排队买奶茶,先排队的人先拿到商品,队列负责缓冲和维持顺序。
3. 两种主流标准:AMQP vs JMS
| 标准 | 本质 | 语言限制 | 核心特点 |
|---|---|---|---|
| AMQP | 网络传输协议(类似 HTTP) | 跨语言,和开发语言无关 | 统一消息传输格式,消息模式丰富 |
| JMS | Java 平台的 API 接口 | 仅支持 Java 语言 | 统一 Java 端的操作接口 |
RabbitMQ 就是基于 AMQP 协议实现的。
4. 常见消息队列产品
- ActiveMQ:基于 JMS 的老牌消息中间件
- RabbitMQ:基于 AMQP,Erlang 语言开发,稳定性极强
- RocketMQ:阿里开源,基于 JMS,适合电商业务场景
- Kafka:分布式高吞吐消息系统,适合日志采集、大数据场景
二、RabbitMQ 核心概念
RabbitMQ 是基于 AMQP 协议的开源消息中间件,采用 Erlang 语言开发,天生支持高并发与分布式架构,以稳定可靠著称。
核心架构组件:
- Broker:RabbitMQ 服务本身,负责接收、存储、转发消息的服务器。
- Virtual Host(vhost):虚拟命名空间,类似 MySQL 的数据库。多租户场景下,不同业务使用不同 vhost,数据和权限完全隔离。
- Connection:生产者/消费者与 Broker 之间建立的 TCP 连接。
- Channel(信道):建立在 Connection 内部的轻量级逻辑连接。多线程共用一个 TCP 连接,每个线程对应一个 Channel,大幅减少建立 TCP 连接的资源开销。
- Exchange(交换机):消息的"分拣中心",接收生产者发送的消息,根据绑定规则分发到对应队列。
- Queue(队列):消息的"暂存柜",消息最终存储在这里,等待消费者拉取消费。
- Binding(绑定):交换机与队列之间的关联规则,包含 routing key,交换机根据该规则分发消息。
三、Docker 安装 RabbitMQ
一键启动带 Web 管理后台的版本:
shell
docker run -d \
--name rabbitmq \
-p 5672:5672 \ # 客户端通信端口
-p 15672:15672 \ # Web 管理后台端口
-e RABBITMQ_DEFAULT_USER=guest \
-e RABBITMQ_DEFAULT_PASS=123456 \
rabbitmq:3.13-management
启动后访问 http://服务器IP:15672 即可进入可视化管理后台。
四、RabbitMQ 五种工作模式
1. 简单模式(Hello World)
- 结构:一个生产者 → 默认交换机 → 一个队列 → 一个消费者
- 特点:最简单的点对点模式,无需手动创建交换机,使用系统默认交换机。
- 生产例子:单应用日志采集,日志服务只接收一个业务系统的日志,一对一发送处理。
2. 工作队列模式(Work Queues)
- 结构:一个生产者 → 默认交换机 → 一个队列 → 多个消费者
- 特点:多个消费者竞争消费同一个队列的消息,消息默认轮询分发,一条消息只会被一个消费者处理。
- 生产例子:批量生成报表任务,1000 个报表任务发送到队列,部署 3 个消费者实例,每个处理约 333 个任务,处理速度提升 3 倍。
3. 发布订阅模式(Fanout 交换机)
- 结构:生产者 → Fanout 交换机 → 多个队列 → 多个消费者
- 特点:广播模式,交换机会把消息转发给所有绑定自身的队列,每个队列都能收到完整消息。
- 生产例子:用户支付成功事件。支付服务发送消息到 Fanout 交换机,库存服务、物流服务、财务服务、短信通知服务各自绑定队列,都能收到支付成功消息,分别处理扣库存、发货、记账、发短信的逻辑,服务之间完全解耦。
4. 路由模式(Direct 交换机)
- 结构:生产者 → Direct 交换机 → 匹配 routing key 的队列 → 消费者
- 特点:精确匹配,只有队列绑定的 routing key 与消息的 routing key 完全一致,才能收到消息。
- 生产例子 :订单取消事件。消息 routing key 设为
order.cancel,只有库存服务(回滚库存)、优惠券服务(退回优惠券)的队列绑定了该 key;物流、财务服务收不到消息,避免无效的业务处理。
5. 主题模式(Topic 交换机)
- 结构:生产者 → Topic 交换机 → 通配符匹配 routing key 的队列 → 消费者
- 匹配规则 :
*:匹配恰好 1 个单词#:匹配 0 个或多个单词- 单词之间用
.分隔,例如order.pay.success
- 生产例子 :全链路异常监控系统。
- 绑定
#.error的队列:接收所有服务、所有模块的错误消息,统一告警 - 绑定
order.*.*的队列:接收所有订单相关消息,做订单全链路统计
- 绑定
五、消息可靠性投递
消息从生产到消费有三个容易丢消息的环节,对应三套解决方案:
1. 生产者端:消息确认机制(Confirm + Return)
解决问题:消息发送后,没到达交换机,或者到了交换机但没路由到队列,生产者无法感知。
- Confirm 确认 :消息到达交换机后,Broker 给生产者回调。
ack=true表示成功到达交换机;ack=false表示投递失败。 - Return 退回:消息到达交换机,但找不到匹配的队列,Broker 将消息和失败原因返回给生产者。
- 生产例子:支付回调消息必须确保投递成功。发送后如果收到 nack 或 return,自动重试重发;超过 3 次失败则记录告警,人工介入处理,避免用户支付后订单状态不更新。
2. Broker 端:持久化机制
解决问题:RabbitMQ 服务器宕机重启,内存中的交换机、队列、消息全部丢失。
- 默认情况下,交换机、队列、消息都是持久化的,会写入磁盘。
- 创建队列、交换机时勾选持久化(durable=true),消息就会落盘,服务重启后数据不丢失。
- 生产例子:核心订单消息全部开启持久化,哪怕机房断电重启,待处理的订单消息也不会丢失,服务恢复后继续处理。
3. 消费者端:手动消息确认(ACK 机制)
解决问题:消费者拿到消息,还没处理完服务就崩溃,消息直接丢失。
- 默认是自动确认:消费者拿到消息就自动告诉 Broker"已收到",Broker 直接删除消息。
- 改成手动确认 :业务处理成功,调用
basicAck()通知 Broker 删除消息;处理失败,调用basicNack()或basicReject()。requeue=true:将消息放回队列,重新投递重试requeue=false:直接丢弃,或转入死信队列
- 常用策略:判断消息是否首次投递,第一次失败则重发一次;第二次仍失败就拒绝不再重发,避免死循环。
- 生产例子:扣库存操作,第一次因数据库网络波动失败,消息重发再试一次;如果仍然失败,进入异常队列人工排查,防止一直重试导致重复扣库存。
六、消费端限流
作用:防止 MQ 中积压的消息一次性全部推给消费者,压垮消费者服务。
- 配置
prefetch参数:设置消费者每次最多从 Broker 拉取的消息数量,处理完一批、确认后再拉下一批。 - 生产例子 :消费者服务配置较低(1核2G),无法同时处理大量消息。设置
prefetch=50,每次只拉取 50 条,处理完再拉取,保证服务稳定运行,不会出现内存溢出。
七、消息超时(TTL)
TTL(Time To Live)即消息存活时间,消息过期后仍未被消费,会被自动清除。
两种设置方式:
- 队列级别:给整个队列设置统一过期时间,队列内所有消息生效。
- 消息级别:给单条消息单独设置过期时间,优先级高于队列级别。
- 生产例子:手机验证码 5 分钟内有效。给验证码消息设置 TTL=300000ms(5 分钟),过期自动删除,无需额外开发定时任务清理无效验证码。
八、死信队列(DLX)
1. 什么是死信
无法被正常消费的消息称为"死信"。消息变成死信的三种场景:
- 消费者拒绝消息(basicNack/basicReject),且
requeue=false不重回队列 - 队列消息数达到上限,新消息进入时最早的消息被挤出(溢出)
- 消息 TTL 过期,仍未被消费
2. 死信队列原理
给普通队列绑定一个死信交换机(DLX) ,消息变成死信后,会被自动转发到死信交换机,再路由到专门的死信队列,由独立的消费者处理。
3. 生产例子
订单超时自动取消:
- 用户下单时,发送消息到普通队列,设置 TTL=30 分钟
- 30 分钟内用户完成支付,消费者正常消费并确认消息
- 30 分钟未支付,消息过期变成死信,进入死信队列
- 死信队列的消费者收到消息,自动执行取消订单、回滚库存、退回优惠券的逻辑
九、延迟队列
1. 概念
延迟队列:消息发送后,不立即让消费者消费,等待指定时长后再投递消费。
RabbitMQ 原生没有直接的延迟队列功能,主流有两种实现方案。
2. 实现方案
方案一:TTL + 死信队列
即上述订单超时的例子,利用"消息超时转为死信"的特性,间接实现延迟处理。
- 优点:无需安装插件,原生支持
- 缺点:不同延迟时间的消息需要对应不同队列,灵活度低
方案二:延迟消息插件
安装官方插件 rabbitmq_delayed_message_exchange,支持 x-delayed-message 类型交换机,可灵活设置每条消息的延迟时间,最长支持延迟 2 天。
- 优点:同一个队列可容纳不同延迟时间的消息,使用更灵活
- 生产例子:多场景超时提醒:订单 30 分钟未支付取消、发货 7 天自动确认收货、会员到期前 3 天提醒,都可以共用同一个延迟交换机,各自设置延迟时间。
十、生产级:消息百分百投递方案
1. 核心思路
数据库落库 + 状态标记 + 定时任务重试 + 消费端幂等,从机制上保证消息不丢失、不重复消费。
2. 投递流程
- 预存库:发送消息前,先将消息内容、状态(0=待发送)存入数据库消息表。
- 发消息:生产者发送消息到 RabbitMQ。
- 更新状态:收到 Broker 的 Confirm 确认后,将消息状态更新为 1(已发送)。
- 定时重试:定时任务每隔几分钟扫描消息表,找出超时未确认的消息,重新发送。
- 失败兜底:超过最大重试次数(如 3 次),标记为 2(发送失败),触发告警通知人工处理。
3. 消费端幂等性保障
幂等性:同一条消息消费 1 次和消费 N 次,业务结果完全一致。
因网络重试等原因,消息可能重复投递,必须保证幂等。
- 常用方案:乐观锁机制
- 消息携带业务 ID 和版本号 version
- 处理业务时,以版本号为条件:
update 账户表 set 余额=余额-500, version=version+1 where id=订单ID and version=1 - 第一次执行成功,version 变为 2;第二次重复消息执行时,where 条件不匹配,不会重复扣款
- 生产例子:支付扣减余额场景,用乐观锁保证同一条支付消息重复投递,也不会出现重复扣款。