199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# Room Outbox Worker
|
||
|
||
本文档定义当前 `room-service` outbox 的实际语义。房间写命令只负责提交 Room Cell 状态、command log、snapshot 和 `room_outbox`;腾讯云 IM、activity-service、notice-service、后续 push/inbox 等外部副作用都通过 outbox worker 或 RocketMQ 下游 consumer 异步完成。
|
||
|
||
## 核心结论
|
||
|
||
| 项目 | 当前规则 |
|
||
| --- | --- |
|
||
| 状态 owner | Room Cell 和 MySQL snapshot/command log 是房间状态权威来源 |
|
||
| 持久事实 | `room_outbox` 是房间外事件的持久事实源,和房间命令提交链路一起落 MySQL |
|
||
| 发布模式 | `outbox_worker.publish_mode` 支持 `direct`、`mq`、`dual` |
|
||
| MQ 职责 | RocketMQ 只做事件分发和宝箱延迟唤醒,不保存房间核心状态 |
|
||
| 下游语义 | 下游服务按 `event_id` 幂等消费,失败由自己的位点、重试和死信处理 |
|
||
| 主链路 | HTTP/gRPC 请求不等待 IM REST、activity、notice、push 或 inbox 投递完成 |
|
||
|
||
不做的事:
|
||
|
||
| 不做 | 原因 |
|
||
| --- | --- |
|
||
| 在 Room Cell 中保存投递状态 | 投递状态属于 outbox 或下游消费位点,不属于房间权威状态 |
|
||
| 用 Redis 替代 outbox | Redis 只保存热状态和短 TTL lease,不能作为恢复事实来源 |
|
||
| 用 RocketMQ 替代 `room_outbox` | 房间状态提交成功但 MQ 发布失败时,必须能从 MySQL 补偿发布 |
|
||
| 追求精确一次投递 | 外部系统和进程崩溃窗口只能做到 at-least-once,消费者必须按 `event_id` 去重 |
|
||
|
||
## 写命令链路
|
||
|
||
```text
|
||
gateway -> room-service command
|
||
-> command_id idempotency check
|
||
-> ensure Redis lease and Room Cell
|
||
-> mutate cloned RoomState
|
||
-> optional wallet debit before room gift state is committed
|
||
-> SaveMutation(command log + room_outbox + room meta)
|
||
-> snapshot when version hits snapshot_every_n
|
||
-> update list/presence projections best effort
|
||
-> return response
|
||
|
||
outbox worker
|
||
-> claim pending/retryable rows
|
||
-> if RoomTreasureCountdownStarted, schedule treasure open delayed wakeup when configured
|
||
-> publish by mode:
|
||
direct: Tencent IM publisher + activity gRPC publisher
|
||
mq: RocketMQ room_outbox publisher
|
||
dual: direct publishers + RocketMQ publisher
|
||
-> delivered / retryable / failed
|
||
```
|
||
|
||
主请求成功只表示房间状态已经提交;不表示腾讯云 IM、activity-service、notice-service 或任何后续通知已经投递完成。客户端实时表现应以请求响应、房间 snapshot 和腾讯云 IM 系统消息共同补偿。
|
||
|
||
## 发布模式
|
||
|
||
| `publish_mode` | 行为 | 适用场景 |
|
||
| --- | --- | --- |
|
||
| `direct` | worker 直接调用腾讯云 IM publisher 和 activity gRPC publisher;所有 direct publisher 成功后才标记 `delivered` | 本地开发、未接 MQ 的隔离测试 |
|
||
| `mq` | worker 只把 `room_outbox_event` 发布到 `hyapp_room_outbox`;RocketMQ 接收成功后标记 `delivered` | 线上推荐模式,activity、notice、IM bridge 各自 consumer group 解耦消费 |
|
||
| `dual` | 同时 direct 投递和 MQ 发布,全部成功后标记 `delivered` | 迁移或灰度期;要求所有消费端都能按 `event_id` 去重 |
|
||
|
||
在 `mq` 模式下,`room_outbox.status=delivered` 的含义是“事件已经可靠进入 RocketMQ”,不是“所有下游都已处理成功”。下游的成功、重试和死信由各自服务的消费位点负责,不能反写 `room_outbox.status`。
|
||
|
||
## 数据状态
|
||
|
||
`room_outbox.status` 使用:
|
||
|
||
| 状态 | 含义 |
|
||
| --- | --- |
|
||
| `pending` | 已落库,尚未被 worker 处理 |
|
||
| `delivering` | 已被 worker 抢占,`lock_until_ms` 前其他 worker 不抢 |
|
||
| `delivered` | 当前发布模式要求的 publisher 已处理成功;MQ 模式下表示 RocketMQ 已接收 |
|
||
| `retryable` | 上次发布失败,等待 `next_retry_at_ms` 后重试 |
|
||
| `failed` | 达到最大重试次数,进入死信,需要后台工具查看或重放 |
|
||
|
||
旧数据里可能存在历史 `published` 状态。当前 worker 不再写这个状态,新链路只写上表状态。
|
||
|
||
## RocketMQ Topics
|
||
|
||
| Topic | Producer | Consumer Group | 语义 |
|
||
| --- | --- | --- | --- |
|
||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-activity-room-outbox` | activity-service 消费房间事实,做活动、播报、系统消息等后续处理 |
|
||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-notice-room-outbox` | notice-service 消费私有通知相关事实,例如 `RoomUserKicked` |
|
||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 |
|
||
| `hyapp_room_treasure_open` | `room-service` treasure scheduler | `hyapp-room-treasure-open` | 宝箱倒计时到点唤醒 Room Cell 开箱命令 |
|
||
|
||
`hyapp_room_treasure_open` 不是业务状态源。消息只携带 `app_code、room_id、box_id、level、open_at_ms、command_id` 等唤醒参数;真正能否开箱仍由 room-service 加载 Room Cell 后校验当前宝箱状态、等级、`box_id`、`open_at_ms` 和 UTC 重置边界。
|
||
|
||
## 配置
|
||
|
||
`services/room-service/configs/*.yaml` 需要同时维护本地、Docker 和腾讯云示例配置:
|
||
|
||
```yaml
|
||
rocketmq:
|
||
enabled: false
|
||
name_servers: []
|
||
name_server_domain: ""
|
||
access_key: ""
|
||
secret_key: ""
|
||
namespace: ""
|
||
send_timeout: "3s"
|
||
retry: 2
|
||
room_outbox:
|
||
enabled: false
|
||
topic: "hyapp_room_outbox"
|
||
producer_group: "hyapp-room-outbox-producer"
|
||
tencent_im_consumer_enabled: false
|
||
tencent_im_consumer_group: "hyapp-room-im-bridge"
|
||
consumer_max_reconsume_times: 16
|
||
treasure_open:
|
||
enabled: false
|
||
topic: "hyapp_room_treasure_open"
|
||
producer_group: "hyapp-room-treasure-open-producer"
|
||
consumer_group: "hyapp-room-treasure-open"
|
||
consumer_max_reconsume_times: 16
|
||
|
||
outbox_worker:
|
||
enabled: true
|
||
publish_mode: "direct"
|
||
poll_interval: "1s"
|
||
batch_size: 100
|
||
publish_timeout: "3s"
|
||
retry_strategy: "exponential_backoff"
|
||
max_retry_count: 10
|
||
initial_backoff: "5s"
|
||
max_backoff: "5m"
|
||
```
|
||
|
||
配置规则:
|
||
|
||
| 字段 | 规则 |
|
||
| --- | --- |
|
||
| `rocketmq.enabled` | 控制 RocketMQ client 是否启用;本地默认关闭,线上示例开启 |
|
||
| `rocketmq.room_outbox.enabled` | 控制 room outbox 是否可发布到 MQ;`publish_mode=mq/dual` 时必须开启 |
|
||
| `rocketmq.room_outbox.tencent_im_consumer_enabled` | 控制 room-service 是否作为 IM bridge consumer 消费 `hyapp_room_outbox` |
|
||
| `rocketmq.treasure_open.enabled` | 控制宝箱倒计时是否发布和消费延迟唤醒消息 |
|
||
| `outbox_worker.publish_mode` | 本地默认 `direct`,线上建议 `mq`,迁移期可用 `dual` |
|
||
| `poll_interval` | worker 空轮询间隔,必须大于 0,避免 busy loop |
|
||
| `batch_size` | 每轮最大抢占条数,必须大于 0,避免全表扫描 |
|
||
| `publish_timeout` | 单条 outbox 发布 deadline,避免外部系统拖死 worker |
|
||
| `max_retry_count` | 达到次数后标记 `failed`,防止 poison event 永久刷日志 |
|
||
|
||
## 投递对象
|
||
|
||
| Publisher / Consumer | 行为 |
|
||
| --- | --- |
|
||
| RocketMQ room outbox publisher | 把 protobuf outbox 包装成统一 `room_outbox_event`,发布到 `hyapp_room_outbox` |
|
||
| Tencent IM direct publisher | `RoomCreated` 对已同步创建的房间群执行幂等 EnsureGroup;房间系统事件发送腾讯云 IM 群自定义消息 |
|
||
| Activity direct publisher | 本地或未接 MQ 时直接调用 activity-service room event 消费入口 |
|
||
| Treasure open scheduler | 在 `RoomTreasureCountdownStarted` 后发布到点延迟消息,唤醒 room-service 开箱 |
|
||
| Activity MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,按 `event_id` 幂等推进活动/播报逻辑 |
|
||
| Notice MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,写 `notice_delivery_events` 后投 C2C 私有通知 |
|
||
| Room IM bridge MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,投递房间群系统消息和群成员控制 |
|
||
|
||
同一个事件允许被多个 consumer group 消费。任何 consumer 失败都不能阻塞 Room Cell 主链路,也不能影响其它 consumer group 的进度。
|
||
|
||
## 重试和死信
|
||
|
||
发布失败处理:
|
||
|
||
```text
|
||
publish failed
|
||
-> retry_count + 1
|
||
-> if retry_count >= max_retry_count: status=failed
|
||
-> else: status=retryable, next_retry_at_ms=now+backoff
|
||
```
|
||
|
||
`failed` 不再自动重试。后台应提供人工查看和重放工具,重放时必须保留原 `event_id` 或生成明确的新修复事件,避免消费端无法去重。
|
||
|
||
RocketMQ consumer 失败时不修改 `room_outbox`。consumer 应依赖 RocketMQ 重投、自己的幂等表和死信处理;对于已经执行业务副作用但 ACK 失败的窗口,下一次消费必须按 `event_id` 安全去重。
|
||
|
||
## 阶段耗时日志
|
||
|
||
每条房间写命令都会输出 `room_command_timing`:
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `idempotency_ms` | command log 幂等检查耗时 |
|
||
| `lease_ms` | Redis lease / Room Cell 装载耗时 |
|
||
| `wallet_debit_ms` | 送礼扣费耗时;非送礼为 0 |
|
||
| `save_mutation_ms` | command log + outbox + 房间 meta 提交耗时 |
|
||
| `snapshot_ms` | 快照持久化耗时;未触发为 0 |
|
||
| `projection_ms` | 列表和 presence 投影耗时 |
|
||
| `activity_ms` | 主链路 activity 同步耗时;当前固定为 0 |
|
||
| `tencent_im_ms` | 主链路腾讯 IM 同步耗时;CreateRoom 记录同步建群耗时,其余命令当前为 0 |
|
||
| `activity_async_outbox` | 本命令是否产生异步 activity/outbox 投递 |
|
||
| `tencent_im_async_outbox` | 本命令是否产生异步 IM/outbox 投递 |
|
||
|
||
排查接口卡顿时先看 `total_ms`,再看各阶段字段。送礼慢通常优先看 `wallet_debit_ms`,普通房间命令慢优先看 `save_mutation_ms`、`snapshot_ms` 和 `projection_ms`。
|
||
|
||
## 验收
|
||
|
||
| 场景 | 验收 |
|
||
| --- | --- |
|
||
| IM 不可用,`direct/dual` | 房间写命令仍成功,outbox 进入 `retryable` 或最终 `failed` |
|
||
| activity 不可用,`direct/dual` | 房间写命令仍成功,outbox 进入重试 |
|
||
| MQ 不可用,`mq/dual` | 房间写命令仍成功,outbox 进入重试;MQ 恢复后补发 |
|
||
| activity consumer 不可用,`mq` | `room_outbox` 不回退;RocketMQ 和 activity 消费位点负责积压与重试 |
|
||
| poison event | 达到 `max_retry_count` 后进入 `failed`,不再每秒刷日志 |
|
||
| 宝箱倒计时 | `RoomTreasureCountdownStarted` 发布延迟唤醒;到点后 room-service 重新校验状态再开箱 |
|
||
| 正常投递 | 同一 `event_id` 可被多个 consumer group 独立消费且重复投递不产生重复副作用 |
|
||
| 真实链路 | create/join/mic/sendGift/leave 均有 `room_command_timing` |
|