# RabbitMQ 配置方案 > **权威源声明**:本文档已与现网代码对齐(2026-05-14)。如本文与代码冲突,**以代码为准**: > - 拓扑创建脚本:[`backend/bin/rabbitmq.sh`](../backend/bin/rabbitmq.sh) > - 用户/凭据配置:[`backend/config/autoload/mq_user.php`](../backend/config/autoload/mq_user.php) + [`backend/config/autoload/amqp.php`](../backend/config/autoload/amqp.php) > - 业务消费者(注解 declare):`backend/app/Platform/{Order,Product,Refund}Consumer.php` > - 错误兜底(双写):[`backend/app/Platform/Traits/FailedMessageTrait.php`](../backend/app/Platform/Traits/FailedMessageTrait.php) + [`backend/app/Platform/ErrorProducer.php`](../backend/app/Platform/ErrorProducer.php) + [`backend/app/Model/FailedMessage.php`](../backend/app/Model/FailedMessage.php) > - 运维状态检查:`php ./bin/hyperf.php app:mq:status [--interval=N] [--watch]`([`backend/app/Command/AppMqStatus.php`](../backend/app/Command/AppMqStatus.php) + [`backend/app/Service/MqStatusService.php`](../backend/app/Service/MqStatusService.php)) **配置层级说明**:`mq_user.php` 从数据库 `platforms` 表动态读取启用的平台名,按 `strtolower(str_replace(' ', '_', $name))` 标准化生成 `user_{platform}` 用户名与 `MQ_PASSWORD_{PLATFORM_UPPER}` env 变量名;`amqp.php` 中 `default_consumer` 用户硬编码为 `user_datahub_consumer`,密码由 env `MQ_PASSWORD_CONSUMER` 注入。 ## 概述 本文档描述 datahub 应用的 RabbitMQ 消息队列架构设计和配置方法。 ### 业务背景 - **应用角色**:datahub 是消息消费者,负责接收来自多个电商平台的数据并入库 - **数据来源**:19 个电商/线下平台(amazon_japan、coupang、dewu、douyin、goofish、jd、kaola、lazada、naver、offline、pdd、rakuten、redbook、shopee、shopify、tmall、wechat、wechat_video、youzan)— 平台列表由 `platforms` 表动态管理,名称按 `strtolower(str_replace(' ', '_', $name))` 标准化 - **数据类型**:订单(orders)、产品(products)、退款(refunds)、库存(inventory) - **隔离需求**:不同平台开发者之间的工作空间需要相互隔离,互不影响 ### 架构特点 - **VHost 隔离**:按应用划分 VHost,使用 `Classic` 队列类型 - **Exchange 隔离**:每个平台独立的业务 Exchange + 错误 Exchange,通过权限正则控制访问(用户只能写自己的 platform.exchange / platform.errors.exchange) - **单队列设计**:每种数据类型一个队列,所有平台共享(`orders.queue` / `products.queue` / `refunds.queue` / `inventory.queue`),保证同 data_type 内严格 FIFO - **独立消费者**:每种数据类型一个 Hyperf Consumer 实例,使用 prefetch + 单条 ACK 模式 - **智能重试**:DLX(死信交换机)+ 延迟重试队列(TTL=5s 自动回流 main.exchange)+ 应用层 ErrorProducer 兜底 - **双层错误持久化**:超过重试上限的消息 **同时**写入 `errors.queue` (MQ, TTL=7d) + DB `failed_messages` 表(人工捞回与统计分析) - **消息持久化**:RabbitMQ 充当"远程平台数据的本地缓存",避免重复调用远端 API --- ## 设计方案总结 ### 核心设计理念 本方案采用 **Topic Exchange + 单队列统一路由 + 单消费者批处理 + 单条 ACK + DLX 智能重试 + 适配器模式**,实现跨平台数据同步的高可靠性、高吞吐与强一致性架构。 ### 关键设计决策 ** 注意 ** 1. 消息内部字段无严格要求,但可以在业务侧进行约束 2. 默认单条消息的最大 Payload 尺寸为 128MB 3. 消息队列为缓存系统,应该尽可能的将批量数据放入 payload\ 4. MQ 存在的意义在于快速接受远程生产的 Message #### 1. 为什么使用单队列? | 问题 | 传统多队列方案 | 本方案(单队列) | |------|--------------|----------------| | **队列数量** | 19 平台 × 4 数据类型 = 76 个队列 | 4 个数据类型队列(数量减少 ~95%,且与平台数量解耦)| | **FIFO 保证** | 同类型数据可能分散在多个队列 | 严格 FIFO,同类型数据都在一个队列 | | **运维复杂度** | 需要监控 76 个队列 | 只需监控 4 个主队列 + 4 个 retry 队列 + 1 个 errors 队列 = 9 个 | | **消费者部署** | 需要 N 个消费者实例 | 只需 4 个消费者实例 | | **新增平台成本** | 新建 4 队列 + 4 binding + 用户/权限 | 仅新建 1 业务 exchange + 1 错误 exchange + 5 binding + 1 用户/权限(队列不动)| **原因**:RabbitMQ 无法在队列内部按 topic 再过滤,拆分队列会造成队列爆炸和业务乱序。 #### 2. 为什么使用单消费者? **原因**:RabbitMQ 在同一个队列内使用多消费者时,必然采用 Round-Robin 均匀分配消息,无法按 topic 分发到不同消费者。 **优势**: - ✅ 保证严格 FIFO 顺序 - ✅ 避免消息错投到错误的消费者 - ✅ 简化部署和运维 **如何实现多平台支持?** 通过消费者内部的**适配器模式**,根据消息的 `platform` 字段分发到不同业务逻辑。 #### 3. 为什么使用 Prefetch + 批处理? **原因**:RabbitMQ 是 **Push 模式**,不提供 Kafka 式的批量拉取 API。 **解决方案**: - 设置 `prefetch_count=100`:RabbitMQ 最多推送 100 条未确认消息 - 消费者自行缓存消息,达到 `batch_size=50` 或 `timeout=3s` 后批量处理 - 实现"伪批量拉取"效果,同时保留单条 ACK 的灵活性 **优势**: - ✅ 提升数据库写入效率(批量插入) - ✅ 控制并发和内存压力 - ✅ 失败消息不影响其他消息(单条 ACK) #### 4. 为什么使用 DLX + 延迟重试队列? **问题**:单队列 FIFO 中,如果某条消息持续失败,会阻塞后续消息消费。 **传统方案的问题**: - ❌ `nack(requeue=true)`:消息回队列头部,无限循环,**永远不会进入 DLX** - ❌ 直接丢弃:数据丢失 - ❌ 立即重试:打爆 API/DB **本方案(DLX + 延迟重试 + 双写错误持久化)**: ``` 消费失败 → Consumer 检查 x-death count ↓ count < AMQP_MAX_RETRIES(默认 3) ↓ Consumer 返回 NACK(requeue=false) ↓ 源 queue 的 x-dead-letter-exchange = dlx.{dtype}, dl-rk = "retry" ↓ dlx.{dtype} (topic) — binding rk="retry" → {dtype}.retry.queue (TTL=5s) ↓ retry.queue 的 x-dead-letter-exchange = main.exchange, dl-rk = "{entity}.retry" ↓ 5s 后死信回流 main.exchange (topic) — binding "{entity}.#" → {dtype}.queue (重新消费, x-death count+1) count >= AMQP_MAX_RETRIES ↓ Consumer 调用 FailedMessageTrait::sendToErrorQueue ↓ ┌─ ErrorProducer.publish → errors.exchange (rk="error") → errors.queue (TTL=7d) ← MQ 兜底 └─ FailedMessage::create → DB `failed_messages` 表 ← DB 兜底 ↓ Consumer 返回 ACK(避免主队列继续重试) ``` **重要说明**: - ⚠️ **必须设置 `requeue=false`**:只有 `requeue=false` 才会触发 DLX 机制(已由 `protected bool $requeue = false` 在所有 Consumer 中固定) - ⚠️ **`requeue=true` 会导致无限循环**:消息直接回到原队列头部,永远不进入 DLX - ✅ **retry 队列自动回流**:TTL 到期后由 RabbitMQ 引擎自动死信回 main.exchange,不需要额外消费者 - ✅ **Consumer 控制重试次数**:通过 `FailedMessageTrait::getRetryCount` 读 `x-death[0].count` - ✅ **错误兜底双写**:超过重试上限时,errors.queue + failed_messages 表**同步双写**,互为备份;监控/告警/捞回需同时覆盖两个数据源 - ❌ **DLX 不直接绑定 errors.queue**:错误进入 errors.queue 完全由应用层 `ErrorProducer` 主动 publish 到 `errors.exchange` 实现,**不**走 DLX 路由分流 **优势**: - ✅ 不会卡 FIFO - ✅ 不会无限重试(通过 max retries 限制) - ✅ 延迟重试避免打爆 API/DB - ✅ 错误消息自动隔离到 error 队列 - ✅ 无需额外 Redis 或手写 retry 逻辑 - ✅ retry 过程完全自动化(依赖 RabbitMQ 的 DLX + TTL 机制) ### 方案优势总结 | 维度 | 优势 | |------|------| | **可靠性** | 消息持久化 + 单条 ACK + 智能重试,避免数据丢失 | | **性能** | Prefetch + 批处理 + 数据库批量写入,提升吞吐 | | **顺序性** | 单队列 FIFO 保证严格顺序 | | **扩展性** | 适配器模式,新增平台只需添加适配器类 | | **运维** | 队列数量少,监控简单,故障排查容易 | | **容错** | DLX 智能重试,失败消息不阻塞主队列 | ### 适用场景 ✅ **适合**: - 多平台数据同步(电商、物流、金融等) - 需要严格 FIFO 顺序 - 需要高可靠性和高吞吐 - 平台数量多(> 5个),数据类型固定(< 10个) ❌ **不适合**: - 实时性要求极高(< 100ms) - 需要按平台独立控制消费速率 - 平台数量少(< 3个),可以使用独立队列 --- ## Requeue 机制详解 ### requeue 参数的作用 在 RabbitMQ 中,当消费者处理消息失败时,可以通过 `basic_nack()` 或 `basic_reject()` 拒绝消息,第三个参数 `requeue` 决定了消息的去向: ```php // requeue=true:消息重新回到原队列(队列头部) $channel->basic_nack($deliveryTag, false, true); // requeue=false:消息进入 DLX(如果配置了),否则丢弃 $channel->basic_nack($deliveryTag, false, false); ``` ### requeue=true 的问题 | 行为 | 后果 | |------|------| | 消息回到原队列头部 | 立即被同一个消费者再次取出 | | 不触发 DLX | 永远不会进入重试队列或错误队列 | | 无延迟 | 瞬间形成高速循环(毫秒级) | | 无计数 | 无法统计重试次数,无法设置上限 | **结果**:消息在 `消费 → 失败 → NACK → 回队列 → 消费` 之间无限循环,CPU 和网络资源被浪费,队列状态显示为空(消息循环太快无法观察)。 ### requeue=false 的正确用法 ``` orders.queue arguments: x-dead-letter-exchange = dlx.orders x-dead-letter-routing-key = retry x-message-ttl = 86400000 (24h) ↓ Consumer 消费失败 (count < max_retries) ↓ basic_nack(requeue=false) ↓ 触发 DLX 机制 ↓ dlx.orders (topic) ↓ binding key = "retry" ↓ orders.retry.queue arguments: x-message-ttl = 5000 (5s) x-dead-letter-exchange = main.exchange x-dead-letter-routing-key = order.retry ↓ 5s 后 TTL 到期,自动死信回 main.exchange ↓ main.exchange (topic) ↓ binding key = "order.#" 匹配 "order.retry" ↓ orders.queue (重新进入主队列,x-death count+1) ``` > 注意:retry queue 的 `x-dead-letter-routing-key` 是 **`{entity_singular}.retry`**(如 `order.retry` / `product.retry`),不保留原 routing key。由于 main.exchange 用通配符 `order.#` 绑定 orders.queue,`order.retry` 能正常匹配回流;但失去了"消息原平台来源"信息(platform 字段已存在 message 体内,不依赖 routing key 追溯)。 ### 环境变量配置 在 `.env` 文件中可以配置以下参数: ```bash # 最大重试次数(默认3次) # 消息重试超过此次数后,会被发送到 errors.queue AMQP_MAX_RETRIES=3 # 调试延迟(秒,默认0) # 设置为 2 可以让每条消息处理延迟2秒,方便在 mq:status 中观察队列状态 # 生产环境应设置为 0 AMQP_CONSUMER_DEBUG_DELAY=0 ``` **使用示例**: ```bash # 开发环境:观察消息流转 AMQP_CONSUMER_DEBUG_DELAY=2 php bin/hyperf.php start # 生产环境:正常运行 AMQP_CONSUMER_DEBUG_DELAY=0 php bin/hyperf.php start ``` --- ## 架构设计 ### 1. VHost 设计 VHost 按业务应用进行划分,便于资源隔离和权限管理。 **当前 VHost**: - `datahub` — 电商数据流转专用(注:早期文档误写为 `datahub`,**现网实际为 `datahub`**) --- ### 2. Exchange 设计 在 `datahub` VHost 中,Exchange 分为三类: #### 主路由 Exchange | Exchange 名称 | 类型 | 持久化 | 用途 | |--------------|------|--------|------| | `main.exchange` | Topic | 是 | **仅作为 retry 回流入口** + Consumer Annotation binding 声明源;**不**作为 producer 入口 | > ⚠ producer 直推 `{platform}.exchange`,**不**经过 main.exchange。main.exchange 的两个用途:① retry queue 的 `x-dead-letter-exchange` 指向它,retry 消息通过它回流业务队列;② Hyperf `#[Consumer(exchange: "main.exchange", routingKey: "{entity}.#", queue: "{dtype}.queue")]` 注解声明 binding 时用它。 #### 平台业务 Exchange(producer 入口) 每个平台一个独立的 Topic Exchange,共 19 个(与 `platforms` 表 enabled 平台对齐): | 平台 (`name` 标准化后) | Exchange | |---|---| | amazon_japan | `amazon_japan.exchange` | | coupang | `coupang.exchange` | | dewu | `dewu.exchange` | | douyin | `douyin.exchange` | | goofish | `goofish.exchange` | | jd | `jd.exchange` | | kaola | `kaola.exchange` | | lazada | `lazada.exchange` | | naver | `naver.exchange` | | offline | `offline.exchange` | | pdd | `pdd.exchange` | | rakuten | `rakuten.exchange` | | redbook | `redbook.exchange` | | shopee | `shopee.exchange` | | shopify | `shopify.exchange` | | tmall | `tmall.exchange` | | wechat | `wechat.exchange` | | wechat_video | `wechat_video.exchange` | | youzan | `youzan.exchange` | 全部 `type=topic, durable=true`。 **Routing Key 规范**(`{entity_singular}.{platform}` 格式,注意 entity 单数 / 平台名小写下划线): | 数据类型 | 示例 routing key | |---|---| | 订单 (order) | `order.tmall` / `order.amazon_japan` / `order.wechat_video` | | 产品 (product) | `product.tmall` / `product.amazon_japan` / `product.wechat_video` | | 退款 (refund) | `refund.tmall` / `refund.amazon_japan` / `refund.wechat_video` | | 库存 (inventory) | `inventory.tmall` / `inventory.amazon_japan` / `inventory.wechat_video` | **设计说明**: - producer 直推 `{platform}.exchange`,使用 routing key `{entity_singular}.{platform}` - 业务 binding 把 `{platform}.exchange` 通过 routing key 直绑到 4 个 `{dtype}.queue`(不经过 main.exchange) - 消费者通过消息体 `platform` / `data_type` 字段进行业务分发(**不**依赖 routing key 解析) #### 平台错误 Exchange(应用层错误推送 + 平台错误通知) 每个平台一个独立的错误 Topic Exchange,共 19 个,命名 `{platform}.errors.exchange`: | 来源平台 | Errors Exchange | |---|---| | 19 个平台 | `{platform}.errors.exchange`(amazon_japan / coupang / dewu / douyin / goofish / jd / kaola / lazada / naver / offline / pdd / rakuten / redbook / shopee / shopify / tmall / wechat / wechat_video / youzan)| 每个 `{platform}.errors.exchange` 通过 binding rk=`#` 直连 `errors.queue`。 #### 全局错误 Exchange | Exchange 名称 | 类型 | 持久化 | 用途 | |---|---|---|---| | `errors.exchange` | Topic | 是 | 应用层 `ErrorProducer` 推送目标,binding rk=`#` 直连 `errors.queue` | **Routing Key 规范(应用层 publish 时)**: - 应用层 `ErrorProducer.php` 固定使用 routing key = `error`(见代码) - 平台 producer 在自检/校验失败时可向 `{platform}.errors.exchange` 自定义 routing key(业务侧决定,由 binding `#` 兜底) --- ### 3. Queue 设计 #### 业务队列(主队列) 物理队列数量固定为 4 个,按数据类型划分,每个队列接收所有平台该类型的数据: | Queue 名称 | 持久化 | 自动删除 | 消息 TTL | DLX | 用途 | |-----------|--------|----------|----------|-----|------| | `orders.queue` | 是 | 否 | 24小时 | `dlx.orders` | 接收所有平台的订单数据,单消费者批处理 | | `products.queue` | 是 | 否 | 24小时 | `dlx.products` | 接收所有平台的产品数据,单消费者批处理 | | `refunds.queue` | 是 | 否 | 24小时 | `dlx.refunds` | 接收所有平台的退款数据,单消费者批处理 | | `inventory.queue` | 是 | 否 | 24小时 | `dlx.inventory` | 接收所有平台的库存数据,单消费者批处理 | **队列参数配置**: ```json { "x-message-ttl": 86400000, "x-dead-letter-exchange": "dlx.orders", "x-dead-letter-routing-key": "retry" } ``` #### 死信交换机(DLX) 每种数据类型配备一个 DLX,用于处理失败消息的路由: | Exchange 名称 | 类型 | 持久化 | 用途 | |--------------|------|--------|------| | `dlx.orders` | Topic | 是 | 订单失败消息路由 | | `dlx.products` | Topic | 是 | 产品失败消息路由 | | `dlx.refunds` | Topic | 是 | 退款失败消息路由 | | `dlx.inventory` | Topic | 是 | 库存失败消息路由 | #### 延迟重试队列 为每种数据类型提供延迟重试机制: | Queue 名称 | 持久化 | 消息 TTL | DLX | 用途 | |-----------|--------|----------|-----|------| | `orders.retry.queue` | 是 | 5秒 | `main.exchange` | 订单延迟重试(TTL后自动回到主队列)| | `products.retry.queue` | 是 | 5秒 | `main.exchange` | 产品延迟重试 | | `refunds.retry.queue` | 是 | 5秒 | `main.exchange` | 退款延迟重试 | | `inventory.retry.queue` | 是 | 5秒 | `main.exchange` | 库存延迟重试 | **重试队列参数配置**(按 data_type 实例化,`{entity_singular}` ∈ {`order`, `product`, `refund`, `inventory`}): ```json { "x-message-ttl": 5000, "x-dead-letter-exchange": "main.exchange", "x-dead-letter-routing-key": "{entity_singular}.retry" } ``` 例:`orders.retry.queue` 的 dl-rk = `order.retry`,回流时由 main.exchange 的 `order.#` binding 匹配进入 `orders.queue`。 **重试机制说明(现网实现)**: 1. 消费者处理失败时,调用 `FailedMessageTrait::getRetryCount` 读 `x-death[0].count` 2. 根据重试次数决定处理方式: - **count < `AMQP_MAX_RETRIES`(默认 3)**:`return Result::NACK`(Hyperf 自动按 `$requeue=false` 触发 DLX)→ 进入 `dlx.{dtype}` → 路由到 `{dtype}.retry.queue`(5s 延迟)→ TTL 到期回流 `main.exchange` → 重新进入主队列 - **count >= max_retries**:Consumer 调用 `FailedMessageTrait::sendToErrorQueue`,**同时**: - `ErrorProducer.publish` → `errors.exchange` (rk=`error`) → `errors.queue` (TTL 7d) - `FailedMessage::create` → DB `failed_messages` 表 - 然后 `return Result::ACK`,避免主队列继续重试 3. 配置参数: - **最大重试次数**:`AMQP_MAX_RETRIES`(默认 3) - **延迟时间**:5 秒(retry 队列 `x-message-ttl=5000`) - **调试延迟**:`AMQP_CONSUMER_DEBUG_DELAY`(生产环境设 0) #### 错误队列 统一的错误队列,接收所有超过重试次数的失败消息: | Queue 名称 | 持久化 | 自动删除 | 消息 TTL | 用途 | |-----------|--------|----------|----------|------| | `errors.queue` | 是 | 否 | 7天 | 接收所有类型、所有平台的最终失败消息,供人工排查 | **错误队列的作用和机制**: - ✅ **收集永久失败的消息**:当消息重试次数达到上限(默认3次)后,消费者会主动将消息发送到此队列 - ✅ **避免消息丢失**:即使处理失败多次,消息也会被保存在错误队列中,不会丢失 - ✅ **人工排查和修复**:运维人员可以从错误队列中查看失败原因,修复问题后重新投递 - ✅ **统一管理**:所有数据类型(orders/products/refunds/inventory)的错误消息都集中在一个队列 - ⚠️ **不会自动重试**:错误队列中的消息需要人工介入,不会自动回流到主队列 **错误消息格式**: ```json { "error_id": "err_675d8f3a2b1c4", "original_message": { /* 原始消息内容 */ }, "error": { "type": "InvalidArgumentException", "message": "Missing required field: total_amount", "trace": "Exception stack trace...", "timestamp": "2025-01-15T10:31:00+00:00" }, "metadata": { "platform": "tmall", "platform_id": 2, "company_id": 188, "store_id": 292, "data_type": "order", "failed_at": "2025-01-15T10:31:00+00:00", "retry_count": 3 } } ``` --- ### 4. Binding 配置(现网实际) 本节按"消息流向"组织,每个 binding 都来自本地 MQ 实测(`curl mgmt-api/bindings/datahub`)+ `backend/bin/rabbitmq.sh` 创建逻辑。 #### 4.1 平台业务 Exchange → 业务队列(producer 主路径) 每个 `{platform}.exchange` 通过 4 条 binding 直接绑定到 4 个业务队列: ``` {platform}.exchange (topic) ├── rk = "order.{platform}" → orders.queue ├── rk = "product.{platform}" → products.queue ├── rk = "refund.{platform}" → refunds.queue └── rk = "inventory.{platform}" → inventory.queue ``` 19 平台 × 4 = **76 条 binding**。 #### 4.2 main.exchange → 业务队列(retry 回流路径) ``` main.exchange (topic) ├── rk = "order.#" → orders.queue ├── rk = "product.#" → products.queue ├── rk = "refund.#" → refunds.queue └── rk = "inventory.#" → inventory.queue ``` **4 条 binding**。这是 retry queue 死信回流的唯一入口;producer **不**通过 main.exchange 投递消息。 #### 4.3 DLX → retry queue(仅一种绑定) ``` dlx.orders (topic) — rk = "retry" → orders.retry.queue dlx.products (topic) — rk = "retry" → products.retry.queue dlx.refunds (topic) — rk = "retry" → refunds.retry.queue dlx.inventory (topic) — rk = "retry" → inventory.retry.queue ``` **4 条 binding**。 > ⚠ **DLX 不绑定到 errors.queue**。错误进入 errors.queue 由应用层 `ErrorProducer` 主动 publish 实现(见 §4.5),**不**通过 DLX 路由分流。这一点与早期文档草稿("dlx.{dtype} → errors.queue (rk=error)" 双绑模式)不符,**以现网实现为准**。 #### 4.4 retry queue → main.exchange(TTL 死信回流) retry queue 不需要显式 binding;其 `x-dead-letter-exchange = main.exchange` 与 `x-dead-letter-routing-key = {entity_singular}.retry` 定义在队列 arguments 中,TTL 到期由 RabbitMQ 引擎自动死信投递到 main.exchange。 ``` orders.retry.queue —(TTL 5s, dl-rk=order.retry)→ main.exchange →[order.#]→ orders.queue products.retry.queue —(TTL 5s, dl-rk=product.retry)→ main.exchange →[product.#]→ products.queue refunds.retry.queue —(TTL 5s, dl-rk=refund.retry)→ main.exchange →[refund.#]→ refunds.queue inventory.retry.queue —(TTL 5s, dl-rk=inventory.retry)→ main.exchange →[inventory.#]→ inventory.queue ``` #### 4.5 errors exchange → errors.queue(双入口扇入) ``` errors.exchange (topic) — rk = "#" → errors.queue (全局兜底入口,应用层 ErrorProducer 用) {platform}.errors.exchange (topic) — rk = "#" → errors.queue × 19 (平台自定义错误投递入口) ``` **20 条 binding**(19 平台 + 1 全局)。 #### 4.6 binding 总数核对 | 类别 | 数量 | |---|---| | 平台业务 exchange → 4 业务队列 | 19 × 4 = 76 | | main.exchange → 4 业务队列 | 4 | | dlx.{dtype} → retry queue | 4 | | errors exchange → errors.queue(19 平台 + 1 全局) | 20 | | **总计** | **104** | (不含 default exchange `(AMQP default)` 自动给每个 queue 创建的同名 binding) --- ## 消费者设计 ### 1. 消费者模型 采用 **单消费者 + 批处理 + prefetch + 适配器模式**: | 数据类型 | 消费者 | Queue | 并发模式 | 处理模式 | |---------|--------|-------|----------|---------| | 订单 | Order Consumer | `orders.queue` | 单消费者 | 批处理 + 适配器 | | 产品 | Product Consumer | `products.queue` | 单消费者 | 批处理 + 适配器 | | 退款 | Refund Consumer | `refunds.queue` | 单消费者 | 批处理 + 适配器 | | 库存 | Inventory Consumer | `inventory.queue` | 单消费者 | 批处理 + 适配器 | **设计原则**: - ✅ **单消费者**:每个队列只有一个消费者实例,保证严格 FIFO 顺序 - ✅ **批处理**:积累一批消息后统一处理,提升数据库写入效率 - ✅ **单条 ACK**:每条消息独立确认,失败不影响其他消息 - ✅ **适配器模式**:消费者内部根据平台字段分发到不同业务逻辑 **消费侧业务可以根据业务负载的强度,灵活调整 prefetch 配置,或者更进一步使用 hyperf 多 worker 模式提高吞吐 ** ### 2. Prefetch 配置 ```json { "prefetch_count": 100, "batch_size": 50, "batch_timeout": 3000 } ``` **参数说明**: - `prefetch_count=100`:RabbitMQ 最多推送 100 条未确认消息到消费者 - `batch_size=50`:消费者积累 50 条消息后触发批处理 - `batch_timeout=3000`:如果3秒内未达到 batch_size,也触发处理 **工作流程**: 1. RabbitMQ 按 Push 模式推送最多 100 条消息到消费者 2. 消费者缓存消息,达到 50 条或超时 3 秒后批量处理 3. 批量处理时,仍然对每条消息单独 ACK/NACK 4. 实现"伪批量拉取"效果,同时保留单条确认的灵活性 ### 3. 消费者处理逻辑(现网实现简化版) 完整实现见 [`backend/app/Platform/OrderConsumer.php`](../backend/app/Platform/OrderConsumer.php) 与 [`FailedMessageTrait.php`](../backend/app/Platform/Traits/FailedMessageTrait.php)。简化骨架: ```php #[Consumer(exchange: "main.exchange", routingKey: "order.#", queue: "orders.queue", pool: "default_consumer", nums: 1, enable: true)] class OrderConsumer extends ConsumerMessage { use FailedMessageTrait; protected ?array $qos = ['prefetch_size' => 0, 'prefetch_count' => 1, 'global' => false]; protected bool $requeue = false; // 关键:让失败消息进 DLX 而不是 requeue public function getQueueBuilder(): QueueBuilder { return (new QueueBuilder()) ->setQueue($this->getQueue()) ->setArguments([ 'x-dead-letter-exchange' => ['S', 'dlx.orders'], 'x-dead-letter-routing-key' => ['S', 'retry'], 'x-message-ttl' => ['I', 86400000], // 24h 'x-queue-type' => ['S', 'classic'], ]); } public function consumeMessage($data, AMQPMessage $message): Result { $retry_count = $this->getRetryCount($message); // 读 x-death[0].count $max_retries = (int) env('AMQP_MAX_RETRIES', 3); try { // 1. 创建 EntityParse、entityMatch、entityMap、upsert $parse = EntityParseFactory::createFromMessage($message); $entity = $parse->entityMatch($data['meta']); $rows = $parse->entityMap($data['data'])->all(); Db::beginTransaction(); $entity->newQuery()->upsert($rows, $parse->getUniqueBy(), $parse->getUpdateFields()); // 2. 子项处理、聚合补刷等业务逻辑 ... Db::commit(); return Result::ACK; } catch (Throwable $error) { Db::rollBack(); Log::get()->error('Consumer processing failed', [...]); if ($retry_count >= $max_retries) { // 双写兜底:MQ errors.queue + DB failed_messages 表 $this->sendToErrorQueue($message, $error); return Result::ACK; // 防止主队列继续重试 } return Result::NACK; // requeue=false → 进 DLX → retry queue } } } ``` `FailedMessageTrait::sendToErrorQueue` 内部: ```php $producer = ApplicationContext::getContainer()->get(Producer::class); $producer->produce(new ErrorProducer($message, $error, $retry_count)); // ↓ ErrorProducer 注解 #[Producer(exchange: 'errors.exchange', routingKey: 'error')] // → errors.exchange → errors.queue (MQ 兜底, 7d TTL) FailedMessage::query()->create([...]); // → DB failed_messages 表 (DB 兜底) ``` ### 4. 重试机制工作流程(现网实现) ``` ┌─────────────────┐ │ orders.queue │ ← 消费者从这里接收消息 └────────┬────────┘ │ ├─ 成功 → return Result::ACK │ └─ 失败 → FailedMessageTrait::getRetryCount($message) (读 x-death[0].count) │ ├─ count < AMQP_MAX_RETRIES (默认 3) │ │ │ └→ return Result::NACK (Hyperf 自动按 $requeue=false 处理) │ │ │ ▼ 触发 DLX │ dlx.orders (topic) ──rk="retry"──► orders.retry.queue │ │ TTL=5s │ ▼ │ main.exchange (rk="order.retry") │ │ │ ▼ 命中 binding "order.#" │ orders.queue (重新消费,x-death.count+1) │ └─ count >= AMQP_MAX_RETRIES │ └→ FailedMessageTrait::sendToErrorQueue($message, $error) │ ├──► ErrorProducer.publish → errors.exchange (rk="error") │ → errors.queue (TTL 7d, user_ops 捞回) │ └──► FailedMessage::query()->create(...) → DB failed_messages 表 │ ▼ return Result::ACK (避免主队列继续重试) ``` **关键点**: - ✅ **`$requeue=false` 由 Consumer 类属性固定**:让失败消息进入 DLX 而不是 requeue - ✅ **retry 队列自动回流**:RabbitMQ 引擎按 retry queue 的 `x-message-ttl=5000` 自动死信回 main.exchange,不需要额外消费者 - ✅ **`x-death.count` 自动累加**:每次死信 RabbitMQ 自动增加 count;Consumer 通过 `FailedMessageTrait::getRetryCount` 读取 - ✅ **错误兜底双写**:超过重试上限时 errors.queue + failed_messages 表**同步双写**,互为备份 - ⚠️ **DLX 不绑定 errors.queue**:进入 errors.queue 完全由应用层 `ErrorProducer` 主动 publish 实现,**不**走 DLX 路由分流 ### 5. 为什么这样设计? | 特性 | 原因 | 优势 | |------|------|------| | **单队列 FIFO** | RabbitMQ 无法在队列内部按 topic 过滤 | 保证严格 FIFO,不拆分队列造成业务乱序 | | **单消费者** | RabbitMQ 同队列多消费者必然 Round-Robin 分配 | 避免消息错投到错误的消费者 | | **Prefetch + 批处理** | RabbitMQ 是 Push 模式,不支持 Kafka 式批量拉取 | 实现"伪批量拉取",提升吞吐 | | **单条 ACK** | 保留细粒度控制 | 失败消息不影响其他消息 | | **DLX + 延迟重试** | 避免失败消息阻塞队列 | 不会卡 FIFO,不会无限重试 | | **适配器模式** | 多平台业务逻辑隔离 | 扩展性好,易维护 | --- ## 用户权限配置 > **VHost 名称**:现网为 `datahub`(早期草稿误写为 `datahub-app`)。下述权限正则均来自本地 MQ 实测(`curl mgmt-api/permissions`)+ `backend/bin/rabbitmq.sh`,是**现网真实使用**的正则,不要随意改动。 ### 1. 平台开发者账号(19 个) 用户名规范:`user_{platform}`,其中 `{platform}` 是 `platforms` 表 `name` 字段经 `strtolower(str_replace(' ', '_', $name))` 标准化后的结果。 完整列表:`user_amazon_japan` / `user_coupang` / `user_dewu` / `user_douyin` / `user_goofish` / `user_jd` / `user_kaola` / `user_lazada` / `user_naver` / `user_offline` / `user_pdd` / `user_rakuten` / `user_redbook` / `user_shopee` / `user_shopify` / `user_tmall` / `user_wechat` / `user_wechat_video` / `user_youzan` **权限正则模板**(按平台名展开): ``` configure = ^{platform}\.(exchange|errors\.exchange)$ write = ^{platform}\.(exchange|errors\.exchange)$ read = ^{platform}\.errors\..*$ ``` 示例(tmall): ``` configure = ^tmall\.(exchange|errors\.exchange)$ write = ^tmall\.(exchange|errors\.exchange)$ read = ^tmall\.errors\..*$ ``` **密码 env 命名**:`MQ_PASSWORD_{PLATFORM_UPPER}`,其中 `PLATFORM_UPPER` = 平台标准化名转大写。例:`MQ_PASSWORD_TMALL`、`MQ_PASSWORD_AMAZON_JAPAN`、`MQ_PASSWORD_WECHAT_VIDEO`。密码由部署期 render-rabbitmq.sh 生成;由 `mq_user.php` 动态从 `platforms` 表读取启用平台后注入。 ### 2. Datahub 消费者账号 ``` 用户名: user_datahub_consumer 密码: env MQ_PASSWORD_CONSUMER VHost: datahub configure = ^(main\.exchange|errors\.exchange|dlx\..*)|(.*\.queue)$ write = ^(orders|products|refunds|inventory).*\.queue$|(dlx\..*)|(errors\.exchange)|(.*\.errors\.exchange)$ read = ^(main\.exchange|(orders|products|refunds|inventory).*\.queue|dlx\..*)$ ``` > ⚠ 此权限正则比早期文档("只读业务队列")宽松:消费者需要 **configure** main.exchange / errors.exchange / dlx.* / 所有 queue(因为 Hyperf Consumer Annotation 在 worker 启动时 declare 这些资源),需要 **write** errors.exchange + platform.errors.exchange(因为 `ErrorProducer` 走这条),需要 **read** main.exchange + 业务队列 + dlx.*。 ### 3. 运维监控账号 ``` 用户名: user_ops 密码: env MQ_PASSWORD_OPS VHost: datahub configure = ^errors\..*$ write = (留空) read = ^errors\.queue$ ``` 仅能读 `errors.queue` 做人工捞回;与 DB `failed_messages` 表(应用层提供 Admin UI / API 查询)互为冗余。 ### 4. 管理员账号 ``` 用户名: admin 密码: <强密码,仅 ops/dev 持有> Tag: administrator management API: AMQP_ADMIN_USER / AMQP_ADMIN_PASSWORD (见 backend/config/autoload/amqp.php "management" 段) ``` 完整权限,可访问管理界面。**生产环境**仅用于运维操作 / `rabbitmqadmin` CLI / mgmt API;不暴露给应用代码。 --- ## 消息格式规范 ### 1. message_id 设计 message_id 采用结构化 5 段格式(见 [`OrderProducer::generateMessageId`](../backend/app/Platform/OrderProducer.php)): **格式**: ``` {company_id}#{platform_id}#{store_id}#{entity_type}#{unique_id} ``` **字段说明**: | 段 | 含义 | 来源 | |---|---|---| | `company_id` | 公司 ID | producer 输入参数 | | `platform_id` | 平台 ID(数据库主键) | producer 输入参数 | | `store_id` | 店铺 ID | producer 输入参数 | | `entity_type` | 数据实体类型(order/product/refund/inventory),**单数** | 由 `routingKey` 前缀自动派生(`explode('.', $routingKey)[0]`)| | `unique_id` | 业务侧定义的唯一标识 / 时间区间 / batch id 等,**业务侧维护幂等性** | producer 输入参数 | **示例**: ``` 188#2#292#order#68a3f2c4e9b1f ``` 解析:company=188, platform=2 (tmall), store=292, entity=order, unique_id=`68a3f2c4e9b1f`(uniqid()) **分隔符**:`#`(RabbitMQ message_id 字段允许任意字符;不与 routing key 通配符冲突)。 **特性**: - ✅ 幂等性:业务侧通过 `unique_id` 保证同一实体的 message_id 一致(消费端 upsert 唯一键由 EntityParse 配置) - ✅ 可读性:直接从 message_id 看出数据来源 4 段元数据 - ✅ 便于调试:日志 / 监控可直接根据前 4 段过滤 ### 2. 业务消息格式 由 [`OrderProducer::buildMessage`](../backend/app/Platform/OrderProducer.php) 构造(基类,所有 platform Producer 共用): ```json { "message_id": "188#2#292#order#68a3f2c4e9b1f", "timestamp": "2026-05-14T13:43:59+08:00", "platform": "tmall", "data_type": "order", "meta": { "platform_id": 2, "company_id": 188, "store_id": 292, "platform_store_id": null, "unique_id": "68a3f2c4e9b1f", "source_system": "tmall_api", "retry_count": 0, "data_version": 1747203839 }, "data": [ { /* 平台原始 raw_data 元素 1 */ }, { /* 平台原始 raw_data 元素 2 */ } ] } ``` **字段说明**: | 字段 | 类型 | 必需 | 说明 | |---|---|---|---| | `message_id` | string | 是 | 5 段结构化 ID(见 §1)| | `timestamp` | string | 是 | ISO 8601 格式(`date('c')`)| | `platform` | string | 是 | 平台标识(标准化后的小写下划线名);基类默认 `'platform_unkonw'`,子类 `buildMessage` 中覆盖(如 `TmallProductProducer` 覆盖为 `'tmall'`)| | `data_type` | string | 是 | 数据类型(order/product/refund/inventory,**单数**);基类默认 `'order'`,子类覆盖 | | `meta.platform_id` | int | 是 | 平台 ID(数据库主键,来自 `platforms` 表)| | `meta.company_id` | int | 是 | 公司 ID | | `meta.store_id` | int | 是 | 店铺 ID | | `meta.platform_store_id` | string\|null | 否 | 平台侧店铺标识(如有)| | `meta.unique_id` | string | 是 | 业务唯一标识(与 message_id 末段一致)| | `meta.source_system` | string | 是 | 数据来源系统(基类默认 `'origin_unknow'`,子类覆盖如 `'tmall_api'`)| | `meta.retry_count` | int | 是 | 重试次数(初始 0;消费端通过 AMQP `x-death[0].count` 读取真实值,不读 meta)| | `meta.data_version` | int | 是 | 数据版本(Unix 时间戳,用于乱序消费保护)| | `data` | array | 是 | 平台原始数据数组(基类直接传 `$data['raw_data']`)| > ⚠ **字段名是 `meta` 不是 `metadata`**(与早期文档草稿不同)。消费端 `OrderConsumer.php:86` 读 `$data['meta']`。 **关于 `data_version`**:建议使用平台数据的更新时间戳,消费端可据此防止旧版本覆盖新版本;如平台未提供则 `OrderProducer::buildMessage` fallback 到 `time()`。 ### 3. 错误消息格式 由 [`ErrorProducer::buildErrorMessage`](../backend/app/Platform/ErrorProducer.php) 构造(投递到 `errors.exchange` rk=`error`): ```json { "error_id": "err_68f3d2c4abc1f.93214785", "original_message": { /* 完整原始业务消息(含 meta / data)*/ }, "error": { "type": "InvalidArgumentException", "message": "Company with ID 188 not found", "code": 0, "file": "/var/www/app/Platform/Tmall/EntityParse/Product.php", "line": 42, "trace": "Exception stack trace...", "timestamp": "2026-05-14T13:31:00+08:00" }, "metadata": { "platform": "tmall", "platform_id": 2, "company_id": 188, "store_id": 292, "data_type": "order", "message_id": "188#2#292#order#68a3f2c4e9b1f", "failed_at": "2026-05-14T13:31:00+08:00", "retry_count": 3 } } ``` > 注意:错误消息的字段名是 `metadata`(与业务消息的 `meta` 不一致;保留以兼容现有应用代码 `FailedMessageTrait::persistFailedMessage` 的解析)。 **双写到 DB**:错误消息同步写入 [`failed_messages`](../backend/app/Model/FailedMessage.php) 表(model 字段:error_id / data_type / platform / platform_id / company_id / store_id / error_type / error_message / error_code / error_trace / original_message(json) / retry_count / message_id / failed_at / created_at)。Admin 可通过 [`FailedMessageController`](../backend/app/Controller/Api/V1/FailedMessageController.php) 查询。 --- ## RabbitMQ 管理界面操作指引 ### 1. 访问管理界面 ``` URL: http://:15672 用户名: admin(开发期默认 admin/admin;生产期通过 podman secret / env 注入) 密码: <管理员密码> ``` **推荐使用 Hyperf 命令**(避开浏览器,看核心 9 个队列状态): ```bash php ./bin/hyperf.php app:mq:status # 单次输出 php ./bin/hyperf.php app:mq:status --watch --interval=5 # 每 5 秒刷新 ``` 输出包含三组:Business Queues(4 业务)/ Dead Letter Queues (Retry Queues)(4 retry)/ Shared Queues(errors),分别展示 Messages / Consumers / Status。 ### 2. 创建 VHost 1. 点击顶部导航 **Admin** → **Virtual Hosts** 2. 点击 **Add a new virtual host** 3. 输入 VHost 名称:`datahub` 4. 点击 **Add virtual host** ### 3. 创建用户 1. 点击顶部导航 **Admin** → **Users** 2. 点击 **Add a user** 3. 填写用户信息: - **Username**: `user_tmall`(按 `user_{platform}` 规范) - **Password**: `<安全密码>` - **Tags**: (留空,非管理员用户) 4. 点击 **Add user** ### 4. 配置用户权限 1. 在用户列表中,点击用户名(如 `user_tmall`) 2. 滚动到 **Permissions** 部分 3. 选择 VHost:`datahub` 4. 配置权限正则表达式(与现网模板一致): - **Configure regexp**: `^tmall\.(exchange|errors\.exchange)$` - **Write regexp**: `^tmall\.(exchange|errors\.exchange)$` - **Read regexp**: `^tmall\.errors\..*$` 5. 点击 **Set permission** ### 5. 创建 Exchange 1. 点击顶部导航 **Exchanges** 2. 确保选择正确的 VHost:`datahub` 3. 点击 **Add a new exchange** 4. 填写配置: - **Name**: `tmall.exchange` - **Type**: `topic` - **Durability**: `Durable` - **Auto delete**: `No` - **Internal**: `No` 5. 点击 **Add exchange** 重复以上步骤创建:1 个 `main.exchange` + 4 个 `dlx.{dtype}` + 1 个 `errors.exchange` + 每平台 2 个(`{platform}.exchange` 与 `{platform}.errors.exchange`)。 ### 6. 创建 Queue 1. 点击顶部导航 **Queues** 2. 确保选择正确的 VHost:`datahub` 3. 点击 **Add a new queue** 4. 填写配置(以 `orders.queue` 为例): - **Name**: `orders.queue` - **Durability**: `Durable` - **Auto delete**: `No` - **Arguments**: - `x-message-ttl` = `86400000`(24h) - `x-dead-letter-exchange` = `dlx.orders` - `x-dead-letter-routing-key` = `retry` - `x-queue-type` = `classic` 5. 点击 **Add queue** 四个业务队列同模式;retry 队列 arguments: ``` x-message-ttl = 5000 # 5s x-dead-letter-exchange = main.exchange x-dead-letter-routing-key = {entity_singular}.retry # 如 order.retry x-queue-type = classic ``` `errors.queue` arguments: ``` x-message-ttl = 604800000 # 7d x-queue-type = classic ``` ### 7. 创建 Binding 1. 点击顶部导航 **Exchanges** 2. 点击需要绑定的 Exchange(如 `tmall.exchange`) 3. 滚动到 **Bindings** 部分 4. 在 **Add binding from this exchange** 区域填写: - **To queue**: `orders.queue` - **Routing key**: `order.tmall` 5. 点击 **Bind** 为每个平台 Exchange 创建 **4 个 binding**(order/product/refund/inventory)。 `main.exchange` 创建 4 个 binding(rk: `order.#` / `product.#` / `refund.#` / `inventory.#`)。 每个 `{platform}.errors.exchange` + 全局 `errors.exchange` 各创建 1 个绑定到 `errors.queue`(rk=`#`)。 DLX 各创建 1 个绑定到对应 retry queue(rk=`retry`)。**不**绑定 errors.queue。 --- ## 配置脚本 > **生产环境推荐**:使用 `backend/bin/rabbitmq.sh`(与现网完全一致的脚本,从 `platforms` 表动态读取平台列表)。下方独立脚本仅供**临时手工部署 / 故障排查** 使用。 ### 使用 rabbitmqadmin RabbitMQ 提供命令行工具 `rabbitmqadmin`,可用于批量配置。 #### 安装 rabbitmqadmin ```bash # 从 RabbitMQ 管理界面下载 wget http://:15672/cli/rabbitmqadmin chmod +x rabbitmqadmin sudo mv rabbitmqadmin /usr/local/bin/ ``` #### 配置脚本示例 **注意**:本脚本适用于**新版本 rabbitmqadmin**(Rust 版本)。如果您使用的是旧版本(Python),请参考官方文档调整语法。 ```bash #!/bin/bash # RabbitMQ 连接信息 RABBITMQ_HOST="127.0.0.1" RABBITMQ_PORT="15672" RABBITMQ_USER="admin" RABBITMQ_PASS="admin" VHOST="datahub" # 平台列表(与现网 19 个对齐;新增平台时追加即可) PLATFORMS=( "amazon_japan" "coupang" "dewu" "douyin" "goofish" "jd" "kaola" "lazada" "naver" "offline" "pdd" "rakuten" "redbook" "shopee" "shopify" "tmall" "wechat" "wechat_video" "youzan" ) DATA_TYPES=("orders" "products" "refunds" "inventory") # 0. 检测并清理现有 VHost 和用户(如果存在) echo "========================================" echo "开始清理现有配置..." echo "========================================" # 检查 VHost 是否存在 echo "检查 VHost: $VHOST 是否存在..." VHOST_EXISTS=$(rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS list vhosts | grep -w "$VHOST" || true) if [ -n "$VHOST_EXISTS" ]; then echo "警告: VHost '$VHOST' 已存在,将删除所有现有配置..." # 删除 VHost(会自动删除该 VHost 下的所有 Exchanges、Queues、Bindings、权限) rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ delete vhost --name "$VHOST" echo "✓ VHost '$VHOST' 及其所有资源已删除" else echo "VHost '$VHOST' 不存在" fi # 清理所有相关用户账号 echo "" echo "清理用户账号..." # 平台用户列表 ALL_USERS=() for platform in "${PLATFORMS[@]}"; do ALL_USERS+=("user_${platform}") done # 添加其他用户 ALL_USERS+=("user_datahub_consumer") ALL_USERS+=("user_ops") # 删除用户(如果存在) for user in "${ALL_USERS[@]}"; do USER_EXISTS=$(rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS list users | grep -w "$user" || true) if [ -n "$USER_EXISTS" ]; then rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ delete user --name "$user" echo "✓ 删除用户: $user" fi done echo "" echo "========================================" echo "清理完成,开始重新配置..." echo "========================================" echo "" # 1. 创建 VHost echo "创建 VHost: $VHOST" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare vhost --name "$VHOST" # 2. 创建主 Exchange(用于重试消息回流) echo "创建主 Exchange: main.exchange" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare exchange --name "main.exchange" --vhost "$VHOST" \ --type topic --durable true # 3. 创建 DLX (死信交换机) echo "创建死信交换机 (DLX)..." for dtype in "${DATA_TYPES[@]}"; do rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare exchange --name "dlx.${dtype}" --vhost "$VHOST" \ --type topic --durable true echo "✓ 创建 DLX: dlx.${dtype}" done # 4. 创建主业务队列(带 DLX 配置) echo "" echo "创建主业务队列..." for dtype in "${DATA_TYPES[@]}"; do rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare queue --name "${dtype}.queue" --vhost "$VHOST" --durable \ --arguments "{\"x-message-ttl\":86400000,\"x-dead-letter-exchange\":\"dlx.${dtype}\",\"x-dead-letter-routing-key\":\"retry\"}" echo "✓ 创建队列: ${dtype}.queue" done # 5. 创建重试队列(带 TTL 和 DLX 回流配置) echo "" echo "创建重试队列..." for dtype in "${DATA_TYPES[@]}"; do dtype_singular="${dtype%s}" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare queue --name "${dtype}.retry.queue" --vhost "$VHOST" --durable \ --arguments "{\"x-message-ttl\":5000,\"x-dead-letter-exchange\":\"main.exchange\",\"x-dead-letter-routing-key\":\"${dtype_singular}.retry\"}" echo "✓ 创建重试队列: ${dtype}.retry.queue (dl-rk: ${dtype_singular}.retry)" done # 6. 创建错误队列 echo "" echo "创建错误队列..." rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare queue --name "errors.queue" --vhost "$VHOST" --durable \ --arguments '{"x-message-ttl":604800000}' echo "✓ 创建错误队列: errors.queue" # 7. 绑定主 Exchange 到主队列(使用通配符) echo "" echo "绑定主 Exchange 到主队列..." for dtype in "${DATA_TYPES[@]}"; do # 使用 order.#, product.# 等通配符匹配所有平台 rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "main.exchange" --destination "${dtype}.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "${dtype%s}.#" echo "✓ 绑定: main.exchange → ${dtype}.queue (routing_key: ${dtype%s}.#)" done # 8. 绑定 DLX 到重试队列(注意:现网 DLX 不绑定 errors.queue,错误由应用层 ErrorProducer 直接推 errors.exchange) echo "" echo "绑定 DLX 到重试队列..." for dtype in "${DATA_TYPES[@]}"; do rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "dlx.${dtype}" --destination "${dtype}.retry.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "retry" echo "✓ 绑定: dlx.${dtype} → ${dtype}.retry.queue (routing_key: retry)" done # 8b. 创建全局 errors.exchange 并绑定到 errors.queue echo "" echo "创建全局错误 Exchange..." rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare exchange --name "errors.exchange" --vhost "$VHOST" --type topic --durable true rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "errors.exchange" --destination "errors.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "#" echo "✓ 绑定: errors.exchange → errors.queue (routing_key: #)" # 9. 为每个平台创建 Exchange 和 Binding echo "" echo "========================================" echo "创建平台 Exchange 和 Binding..." echo "========================================" for platform in "${PLATFORMS[@]}"; do echo "" echo "处理平台: ${platform}" # 创建平台业务 Exchange rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare exchange --name "${platform}.exchange" --vhost "$VHOST" \ --type topic --durable true echo "✓ 创建业务 Exchange: ${platform}.exchange" # 绑定到业务队列(使用新的 routing key 格式:data_type.platform) rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "${platform}.exchange" --destination "orders.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "order.${platform}" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "${platform}.exchange" --destination "products.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "product.${platform}" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "${platform}.exchange" --destination "refunds.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "refund.${platform}" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "${platform}.exchange" --destination "inventory.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "inventory.${platform}" echo "✓ 绑定到业务队列 (4个)" # 创建平台错误 Exchange rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare exchange --name "${platform}.errors.exchange" --vhost "$VHOST" \ --type topic --durable true echo "✓ 创建错误 Exchange: ${platform}.errors.exchange" # 绑定到错误队列 rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare binding --source "${platform}.errors.exchange" --destination "errors.queue" \ --destination-type queue --vhost "$VHOST" --routing-key "#" echo "✓ 绑定到错误队列" # 创建平台用户 rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare user --name "user_${platform}" --password "change_me_${platform}" --tags "" echo "✓ 创建用户: user_${platform}" # 配置平台用户权限 rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare permissions --vhost "$VHOST" --user "user_${platform}" \ --configure "^${platform}\\.(exchange|errors\\.exchange)$" \ --write "^${platform}\\.(exchange|errors\\.exchange)$" \ --read "^${platform}\\.errors\\..*$" echo "✓ 配置用户权限" done # 10. 创建 datahub 消费者用户 echo "" echo "========================================" echo "创建 datahub 消费者用户..." echo "========================================" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare user --name "user_datahub_consumer" --password "change_me_consumer" --tags "" echo "✓ 创建用户: user_datahub_consumer" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare permissions --vhost "$VHOST" --user "user_datahub_consumer" \ --configure "^(main\\.exchange|errors\\.exchange|dlx\\..*)|(.*\\.queue)$" \ --write "^(orders|products|refunds|inventory).*\\.queue\$|(dlx\\..*)|(errors\\.exchange)|(.*\\.errors\\.exchange)\$" \ --read "^(main\\.exchange|(orders|products|refunds|inventory).*\\.queue|dlx\\..*)\$" echo "✓ 配置用户权限" # 11. 创建运维监控用户 echo "" echo "创建运维监控用户..." rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare user --name "user_ops" --password "change_me_ops" --tags "" echo "✓ 创建用户: user_ops" rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ declare permissions --vhost "$VHOST" --user "user_ops" \ --configure "^errors\\..*$" --write "" --read "^errors\\.queue$" echo "✓ 配置用户权限" echo "" echo "========================================" echo "RabbitMQ 配置完成!" echo "========================================" echo "" echo "已创建:" echo "- 1 个 VHost: $VHOST" echo "- 1 个主 Exchange: main.exchange" echo "- 1 个全局错误 Exchange: errors.exchange" echo "- 4 个 DLX: dlx.orders, dlx.products, dlx.refunds, dlx.inventory" echo "- 4 个主队列: orders.queue, products.queue, refunds.queue, inventory.queue" echo "- 4 个重试队列: orders.retry.queue, products.retry.queue, refunds.retry.queue, inventory.retry.queue" echo "- 1 个错误队列: errors.queue" echo "- $((${#PLATFORMS[@]} * 2)) 个平台 Exchange (${#PLATFORMS[@]} 业务 + ${#PLATFORMS[@]} 错误)" echo "- $((${#PLATFORMS[@]} + 2)) 个用户 (${#PLATFORMS[@]} 个平台用户 + 1 个消费者 + 1 个运维)" echo "" echo "提示: 请修改所有用户的默认密码 (change_me_*)" echo "提示: 验证拓扑:php ./bin/hyperf.php app:mq:status" ``` #### 执行脚本 ```bash chmod +x setup_rabbitmq.sh ./setup_rabbitmq.sh ``` --- ## 监控和维护 ### 1. 监控指标 通过 RabbitMQ 管理界面查看以下关键指标: #### 队列健康状态 1. 点击 **Queues** → 选择队列 2. 关注以下指标: - **Ready**: 待消费消息数(正常应 < 1000) - **Unacked**: 未确认消息数(正常应较低) - **Total**: 总消息数 - **Incoming rate**: 消息入队速率 - **Deliver / Get rate**: 消息消费速率 **告警阈值**: - Ready > 10000:队列堆积严重 - Unacked > 5000:消费者处理缓慢 - Incoming rate >> Deliver rate:消费速度跟不上生产速度 #### Exchange 流量统计 1. 点击 **Exchanges** → 选择 Exchange 2. 查看 **Message rates**: - **Publish in**: 消息发布速率 - **Publish out**: 消息路由到队列的速率 **异常情况**: - Publish in > 0 但 Publish out = 0:可能绑定配置错误 ### 2. 常见问题排查 #### 消息未到达队列 **检查步骤**: 1. 确认 Exchange 存在且类型正确 2. 检查 Binding 配置,确保 routing key 匹配 3. 查看 Exchange 的 **Message rates**,确认消息已发布 4. 检查生产者是否使用了正确的 routing key #### 消费者无法消费 **检查步骤**: 1. 确认用户权限配置正确(Read 权限) 2. 检查队列中是否有消息(Ready > 0) 3. 查看消费者连接状态(Connections 页面) 4. 检查消费者代码是否正确 ACK 消息 #### 错误消息未送达平台开发者 **检查步骤**: 1. 确认错误 Exchange 绑定到 `errors.queue` 2. 检查平台开发者账号的 Read 权限 3. 确认平台开发者订阅了正确的 Exchange 4. 查看 `errors.queue` 中是否有消息 ### 3. 备份和恢复 #### 导出配置 ```bash # 导出所有定义(Exchanges, Queues, Bindings, Users, Permissions) rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ export datahub-backup.json ``` #### 导入配置 ```bash # 从备份恢复配置 rabbitmqadmin -H $RABBITMQ_HOST -P $RABBITMQ_PORT -u $RABBITMQ_USER -p $RABBITMQ_PASS \ import datahub-backup.json ``` ### 4. 性能优化 #### 消费者并发配置 - 增加消费者实例数量 - 每个消费者配置多个 channel(如 5-10 个) - 使用 prefetch_count 限制未确认消息数量 #### 队列性能优化 - 启用 lazy queue(大量消息堆积时): ```bash rabbitmqadmin declare queue name=orders.queue vhost=datahub \ durable=true arguments='{"x-queue-mode":"lazy"}' ``` - 定期清理过期消息(通过 TTL 自动实现) ### 5. 日志查看 ```bash # 查看 RabbitMQ 日志 tail -f /var/log/rabbitmq/rabbit@.log # 查看连接日志 tail -f /var/log/rabbitmq/rabbit@_connection.log ``` --- ## 安全建议 ### 1. 密码策略 - 所有用户使用强密码(至少 16 位,包含大小写字母、数字、特殊字符) - 定期更换密码(建议 3-6 个月) - 不同环境(开发/测试/生产)使用不同密码 ### 2. 网络安全 - 生产环境启用 TLS/SSL 加密连接 - 限制管理界面访问(仅允许内网或 VPN 访问) - 配置防火墙规则: - 5672:AMQP 端口(仅允许应用服务器访问) - 15672:管理界面(仅允许管理员 IP 访问) ### 3. 权限最小化原则 - 平台开发者只能访问自己的 Exchange - 消费者只能读取必要的队列 - 避免赋予不必要的 Configure 权限 ### 4. 审计日志 - 启用 RabbitMQ 审计日志插件 - 定期审查用户操作记录 - 监控异常登录行为 --- ## 新增平台接入流程 > **首选路径**:使用 `backend/bin/rabbitmq.sh add `(自动化、与现网一致);下方手工脚本仅作参考。 新增平台(示例 `` = 标准化后的小写下划线名): ### 1. 创建 Exchange(业务 + 错误) ```bash P="" # 例如 "tiktok_shop" rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare exchange --name "${P}.exchange" --vhost datahub --type topic --durable true rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare exchange --name "${P}.errors.exchange" --vhost datahub --type topic --durable true ``` ### 2. 创建 5 条 Binding(4 业务 + 1 错误) ```bash for DTYPE in orders products refunds inventory; do DTYPE_SINGULAR="${DTYPE%s}" rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare binding --source "${P}.exchange" --destination "${DTYPE}.queue" \ --destination-type queue --vhost datahub \ --routing-key "${DTYPE_SINGULAR}.${P}" done rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare binding --source "${P}.errors.exchange" --destination "errors.queue" \ --destination-type queue --vhost datahub --routing-key "#" ``` ### 3. 创建用户并配置权限 ```bash rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare user --name "user_${P}" --password "" --tags "" rabbitmqadmin -H 127.0.0.1 -P 15672 -u admin -p admin \ declare permissions --vhost datahub --user "user_${P}" \ --configure "^${P}\\.(exchange|errors\\.exchange)\$" \ --write "^${P}\\.(exchange|errors\\.exchange)\$" \ --read "^${P}\\.errors\\..*\$" ``` ### 4. 同步到 backend 1. **DB**:`INSERT INTO platforms (name, enabled, ...) VALUES ('', true, ...)`,确保 `mq_user.php` 动态读取时能匹配到 2. **env**:在 backend 的 `.env`(或 host 端 `datahub-backend.env`)追加 `MQ_PASSWORD_=` 3. **重启 backend**:`systemctl --user restart datahub-backend`,让 Hyperf worker 重新加载 mq_user.php 与 env ### 5. 提供给平台开发者 ``` RabbitMQ 连接信息: - Host: - Port: 5672 - VHost: datahub - Username: user_ - Password: 发布配置: - Exchange: .exchange - Routing Keys: - order. → 发布订单数据 - product. → 发布产品数据 - refund. → 发布退款数据 - inventory. → 发布库存数据 错误投递(可选): - Exchange: .errors.exchange - Routing Key: 任意(由 binding `#` 兜底) - 用途:平台侧主动上报无法处理的消息 / 校验失败的数据 消息格式规范: - 参见「消息格式规范」章节(5 段 message_id;meta 字段单数;data_version 时间戳防乱序) ``` --- ## 附录 ### A. 完整资源清单(与本地 MQ 实测对齐 — 2026-05-14) #### VHost - **总数**:1 个 - `datahub` #### Exchanges | 类别 | 数量 | 名称 | |---|---|---| | 主路由 | 1 | `main.exchange`(topic)| | 全局错误 | 1 | `errors.exchange`(topic)| | DLX | 4 | `dlx.orders` / `dlx.products` / `dlx.refunds` / `dlx.inventory`(全 topic)| | 平台业务 | 19 | `{platform}.exchange` × 19(全 topic)| | 平台错误 | 19 | `{platform}.errors.exchange` × 19(全 topic)| **Exchange 业务总数**:**44 个**(不含 `amq.*` 系统 exchange 7 个) #### Queues | 类别 | 数量 | 名称 + 关键 arguments | |---|---|---| | 业务队列 | 4 | `{dtype}.queue`:ttl=24h, dl-exchange=`dlx.{dtype}`, dl-rk=`retry`, type=classic | | 重试队列 | 4 | `{dtype}.retry.queue`:ttl=5s, dl-exchange=`main.exchange`, dl-rk=`{entity_singular}.retry`, type=classic | | 错误队列 | 1 | `errors.queue`:ttl=7d, 无 DLX, type=classic | **Queue 总数**:**9 个**(与 `php ./bin/hyperf.php app:mq:status` 输出一致) #### Bindings | 类别 | 数量 | 路径 | |---|---|---| | 平台业务 → 业务队列 | 76 | `{platform}.exchange` —rk=`{entity}.{platform}`→ `{dtype}.queue`(19 × 4)| | main → 业务队列 | 4 | `main.exchange` —rk=`{entity}.#`→ `{dtype}.queue` | | DLX → 重试队列 | 4 | `dlx.{dtype}` —rk=`retry`→ `{dtype}.retry.queue`(**仅此一种**,**不**绑定 errors.queue)| | 错误 exchange → errors.queue | 20 | `errors.exchange` 1 条 + `{platform}.errors.exchange` × 19 条,全部 rk=`#` | **Binding 业务总数**:**104 条**(不含 default exchange 自动同名 binding 9 条) > 死信回流路径(retry → main)不是 binding,而是 retry queue 的 `x-dead-letter-exchange` arguments 字段,RabbitMQ 引擎在 TTL 到期时自动投递。 #### Users(vhost `datahub`) | 类别 | 数量 | 命名 | 权限说明 | |---|---|---|---| | 平台开发者 | 19 | `user_{platform}` | 仅写自己的 `{platform}.exchange` / `{platform}.errors.exchange`,读自己的 errors(最小权限)| | Datahub 消费者 | 1 | `user_datahub_consumer` | configure/write/read 见 §"消费者账号"现网正则 | | 运维监控 | 1 | `user_ops` | 仅读 `errors.queue` | | 管理员 | 1 | `admin` | tag=administrator,全权限 | **User 总数**:**22 个** 完整平台用户清单: ``` user_amazon_japan, user_coupang, user_dewu, user_douyin, user_goofish, user_jd, user_kaola, user_lazada, user_naver, user_offline, user_pdd, user_rakuten, user_redbook, user_shopee, user_shopify, user_tmall, user_wechat, user_wechat_video, user_youzan ``` #### 架构统计 | 资源类型 | 数量 | 说明 | |---------|------|------| | VHost | 1 | `datahub` | | Exchange | 44 | 1 main + 1 全局 errors + 4 DLX + 19 平台业务 + 19 平台错误 | | Queue | 9 | 4 业务 + 4 retry + 1 errors | | Binding | 104 | 见上表(不含 default exchange 自动 binding)| | User | 22 | 19 平台 + 1 消费者 + 1 运维 + 1 管理员 | **消息流转路径(双层兜底)**: ``` 生产者 → {platform}.exchange ──rk={entity}.{platform}──► {dtype}.queue ──► Consumer │ 消费失败 (count < max) ↓ ◄──── NACK │ ▼ dlx.{dtype} ──rk=retry──► {dtype}.retry.queue │ TTL=5s ▼ main.exchange (rk={entity}.retry) │ ▼ {dtype}.queue (重新消费, count+1) │ 消费失败 (count >= max) │ FailedMessageTrait::sendToErrorQueue │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ErrorProducer ──► errors.exchange ──► errors.queue FailedMessage::create (MQ 兜底, TTL 7d, user_ops 捞回) (DB 兜底, failed_messages 表) │ ▼ Consumer 返回 ACK (避免主队列继续重试) ``` ### B. 参考资料 - [RabbitMQ 官方文档](https://www.rabbitmq.com/documentation.html) - [RabbitMQ Management Plugin](https://www.rabbitmq.com/management.html) - [Access Control (Authentication, Authorization)](https://www.rabbitmq.com/access-control.html) - [rabbitmqadmin 使用指南](https://www.rabbitmq.com/management-cli.html)