10 KiB
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 去重 |
写命令链路
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 和腾讯云示例配置:
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 的进度。
重试和死信
发布失败处理:
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 |