This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,128 @@
<!-- 78ee2e02-349b-4b66-a20c-03e7ee129a45 -->
---
todos:
- id: "rabbitmq-prefetch"
content: "RabbitMqServiceconnect 去掉 prefetch=10getMessage 增加 singleMessagePrefetch 参数并在 true 时 basic_qos(1)"
status: pending
- id: "eventbus-call"
content: "EventBus::execute 调用 getMessage([$this, 'deal'], true)"
status: pending
- id: "consumer-tag"
content: "可选consumer_tag 加 getmypid() 便于管理台区分多进程"
status: pending
- id: "verify-supervisor"
content: "Supervisor numprocs 配置 + RabbitMQ 管理台确认 N consumer 且 Unacked≤1"
status: pending
isProject: false
---
# EventBus 多进程「每进程一次只处理一条」方案
## 现状与问题
[`slot_console/app/service/RabbitMqService.php`](slot_console/app/service/RabbitMqService.php) 在 `connect()` 里写死了:
```php
$this->setExhcange($type)->setBasicQos(null, 10, null)->setQueue()->queueBind();
```
`prefetchCount = 10` 表示:**每个 consumer 最多预取 10 条未 ack 的消息**。Supervisor 开 N 个 `php webman event:bus` 时,理论上最多有 `N × 10` 条消息被「占住」但未必在处理(尤其处理慢时,其余 9 条会堵在该进程本地)。
[`EventBus.php`](slot_console/app/command/EventBus.php) 当前调用:
```php
$rabbitMqService->getMessage([$this, 'deal']);
```
已是 **manual ack**`no_ack = false``deal()` 末尾 `$message->ack()`),只差把 prefetch 改成 1。
```mermaid
sequenceDiagram
participant Q as console_bus
participant P1 as Process1
participant P2 as Process2
Q->>P1: deliver msg1 (prefetch=1)
Q->>P2: deliver msg2
Note over P1: 处理 msg1未 ack 前不再投递
P1->>Q: ack msg1
Q->>P1: deliver msg3
```
## 推荐改动(对齐 slot_pwa / slot_hub 既有模式)
同仓库已有先例:
- [`slot_pwa/app/service/RabbitMqService.php`](slot_pwa/app/service/RabbitMqService.php) — `getMessage($callback, $qos=false)``$qos=true``basic_qos(null, 1, false)`
- [`slot_hub/plugin/slot/hub/WorkerBusiness.php`](slot_hub/plugin/slot/hub/WorkerBusiness.php) — 消费侧 `getMessage(..., true)`
### 1. 改 `RabbitMqService`
文件:[`slot_console/app/service/RabbitMqService.php`](slot_console/app/service/RabbitMqService.php)
- **connect 阶段**:去掉 `setBasicQos(null, 10, null)`(发布端不需要 prefetch消费端在 `getMessage` 里单独设置更明确)。
- **getMessage**:增加第二/along slot_pwa 风格:
```php
public function getMessage(callable $callback, bool $singleMessagePrefetch = false): void
{
if ($singleMessagePrefetch) {
$this->_channel->basic_qos(null, 1, false); // 每 consumer 未 ack 前最多 1 条
}
// basic_consume + wait 循环保持不变
}
```
- **consumer_tag可选但建议**:由固定 `$queueName . 'consumer'` 改为带 pid便于 RabbitMQ 管理台区分多个 Supervisor 进程,例如 `$this->queueName . '_consumer_' . getmypid()`。同一 channel 内唯一即可,多进程各自独立 connection不会冲突。
### 2. 改 `EventBus` 调用
文件:[`slot_console/app/command/EventBus.php`](slot_console/app/command/EventBus.php)
```php
$rabbitMqService->getMessage([$this, 'deal'], true);
```
仅此一处消费 `console_bus` 的入口需要改;其它只 `sendMessage` 的调用不受影响。
### 3. Supervisor 多进程部署(你已选此方式)
每个 worker 仍是独立 PHP 进程 + 独立 AMQP connection**订阅同一队列** `console_bus`[`MQKeyManagerService::QUEUE_CONSOLE_BUS`](slot_console/app/service/MQKeyManagerService.php)。RabbitMQ 默认 round-robin有 3 个 consumer 就 3 路并行,且配合 prefetch=1 后每个 consumer 同时只「持有」1 条未 ack 消息。
示例(进程数按 CPU/吞吐调整):
```ini
[program:slot_console_event_bus]
command=docker exec -w /app/www/slot/slot_console php82 php webman event:bus
process_name=%(program_name)s_%(process_num)02d
numprocs=3
autostart=true
autorestart=true
```
本地联调可开多个终端各跑一条(与 [`slot_console/doc/lucky_reward_deploy.md`](slot_console/doc/lucky_reward_deploy.md) 一致):
```bash
docker exec -w /app/www/slot/slot_console php82 php webman event:bus
```
## 机制说明(为何这样就够)
| 机制 | 作用 |
| --- | --- |
| `basic_qos(0, 1, false)` | 该 consumer 未 ack 前broker 不再向它推第 2 条 |
| `no_ack = false` + `deal()` 末尾 `ack()` | 处理完成后才释放「占用槽位」 |
| 多进程各 `basic_consume` 同一 queue | broker 在多个 consumer 间分发 |
| 每进程独立 connection | 不共享 channel`getInstance` 单例仅进程内有效 |
**不需要** `exclusive = true`(那会变成单 consumer 独占队列,与多进程目标相反)。
## 多进程额外注意(非本次必改,但上线前心里有数)
1. **同 uid 消息顺序**:多 consumer 后,同一用户的两条 bus 消息可能乱序执行;若某类 event 强依赖顺序,需在 Logic 层加锁/幂等(例如按 `uid` 分布式锁),不能单靠 prefetch。
2. **失败重试**:目前仅 `TYPE_FREE_CREDIT_INIT` 在 catch 里 `nack(true)` 重入队;其它类型异常后仍 `ack()`,多进程不会放大这个问题,但重试策略需业务上接受。
3. **验证**RabbitMQ 管理台 → Queues → `console_bus` → Consumers应看到 N 个 consumer压测时每个 consumer 的 **Unacked** 应 ≤ 1。
## 改动范围
- 必改:[`RabbitMqService.php`](slot_console/app/service/RabbitMqService.php)、[`EventBus.php`](slot_console/app/command/EventBus.php)
- 不改Supervisor 配置可在运维侧按 `numprocs` 调整,不必动 PHP 代码
- 不测 RabbitMQ 真连接的单测可保持现状;若有 mock 消费测试,传入第二参数 `true` 即可