Refactor Kafka publishing without DB outbox

This commit is contained in:
richjohnrambo
2026-09-01 23:00:14 +08:00
parent 18237cf3ec
commit 929f847003
25 changed files with 416 additions and 478 deletions
+6 -14
View File
@@ -722,14 +722,14 @@ IM Server 已接入 `im.message.persist`,用于把在线发送链路和最终
当前处理方式:
1. WS/HTTP 发送入口先同步完成权限校验、业务幂等、`seq_id` 分配和 Topic 游标占用。
2. Kafka 开启时,发送入口先写租户库 `im_kafka_outbox`,由 Outbox worker 可靠生产 `im.message.persist`,客户端仍立即拿到已分配的 `seq_id`。
1. WS/HTTP 发送入口先完成权限校验,并由 topic 内存态分配 `seq_id`。
2. Kafka 开启时,发送入口直接生产 `im.message.persist`,不查库、不落库、不写 DB outbox;客户端在 Kafka 写入成功后拿到已分配的 `seq_id`。
3. Kafka 关闭时,继续走原同步落库路径,兼容本地和未接 Kafka 环境。
4. 消费者按 `message_kind` 区分私聊与群聊:
- `p2p` 写入 `im_messages`。
- `group` 写入 `im_group_messages`。
5. 消费侧复用 `im_kafka_consume_log` 做 `event_id + consumer_group` 幂等。
6. 失败按统一退避重试,超过上限写入 `im_kafka_dead_letters` 并投递 `im.dead.letter`。
6. 失败按统一退避重试 3 次,超过上限先投递 `im.dead.letter`,再由 dead letter consumer 写入 `im_kafka_dead_letters`。
### 11.10 IM Server 已接入的缓存失效消费
@@ -874,17 +874,9 @@ IM Server 已在 `im.admin.command` 中接入用户治理类后台动作。Admin
}
```
### 11.9 IM Server 已接入的 Kafka Outbox Worker
### 11.9 IM Server 已移除 Kafka DB Outbox
IM Server 已接入租户库 `im_kafka_outbox` 通用发送 worker,用于避免“业务库写成功但 Kafka 未发送”的事件丢失。
当前能力:
1. 启动后按 active 租户扫描每个租户库的 `im_kafka_outbox`。
2. 扫描 `pending/failed` 且已到 `next_retry_at` 的事件。
3. 发送成功后标记 `sent`、写入 `sent_at`。
4. 发送失败后标记 `failed`,按 Kafka 配置的 `retry_backoff_ms` 写入下一次重试时间。
5. 首个接入方为 `im.message.persist`,后续文件生命周期、工作台事件、审计事件可复用同一 outbox。
IM Server 不再维护租户库 `im_kafka_outbox`,Kafka 事件统一由发送入口直接生产。生产侧使用 Kafka writer 的 `max_retry_count` 控制发送重试;消费侧使用 `im_kafka_consume_log` 做处理幂等和失败重试,超过上限后进入 `im.dead.letter`。
本轮暂不实现 `im.message.push` 离线 Push Kafka 化,已记录到 [IM 第一至十一章后续增强项](./IM-未实现后续增强项.md)。
@@ -930,7 +922,7 @@ IM Server 已接入 `im.workbench.event` 的首批生产与消费。
### 11.12 IM Server 已接入的审计事件
IM Server 已接入 `im.audit.event` 的可靠生产,审计事件写入租户库 `im_kafka_outbox` 后由 Kafka Outbox worker 发送。
IM Server 已接入 `im.audit.event` 的直接生产,审计事件不写 DB outbox。
当前审计范围:
+3 -12
View File
@@ -54,7 +54,6 @@ SOURCE docs/sql/IM-Kafka基础表SQL.sql;
| 表 | 用途 |
| --- | --- |
| `im_kafka_consume_log` | 消费幂等、重试状态。 |
| `im_kafka_outbox` | IM 侧可靠生产 Kafka 事件。 |
| `im_kafka_dead_letters` | 死信记录与人工处理状态。 |
| `im_messages` | 私聊消息存储。 |
| `im_group_messages` | 群聊消息存储。 |
@@ -886,15 +885,7 @@ GET /v1/internal/admin-query/tenants/default/kafka/dead-letters/{dead_id}
### TC-KAFKA-030 Outbox 可靠投递
目标:验证 IM 侧 Kafka Outbox 防止业务成功但 Kafka 丢事件。
步骤:
1. 临时停止 Kafka。
2. 执行会产生 outbox 的操作,如发送消息、提交工作台事件。
3. 查询 `im_kafka_outbox`。
4. 恢复 Kafka。
5. 等待 outbox worker 投递。
目标:验证 IM 侧 Kafka 直接生产失败时不会产生 DB outbox,调用方收到发送失败。
预期:
@@ -958,7 +949,7 @@ GET /v1/internal/admin-query/tenants/default/kafka/dead-letters/{dead_id}
| 进入死信 | 查 `im_kafka_dead_letters.envelope/last_error`,修复后用重放命令。 |
| 用户没有被踢下线 | 查目标 `user_id` 是否是 IM 用户 ID;是否连接在当前 IM 节点;跨节点踢人是否需要后续 Redis fanout 扩展。 |
| 群治理未生效 | 查 `topic/version/actor_user_id` 是否正确,目标用户是否为群成员。 |
| 消息未落库 | 查 `im_kafka_outbox` 是否 sent,`im.message.persist` 是否消费成功。 |
| 消息未落库 | 查 `im.message.persist` 是否消费成功,以及 `im_kafka_consume_log.last_reason/last_error`。 |
| 附件未绑定 | 查消息是否带附件、`im.file.lifecycle` 是否消费成功。 |
| 内部 API 401 | 查 secret、签名文本、RequestURI、timestamp、nonce 是否一致。 |
| 配置没有刷新 | 查是否发送 `im.config.event` 或 `im.cache.invalidate`,IM 是否清理了对应 namespace。 |
@@ -1023,7 +1014,7 @@ GET /v1/internal/admin-query/tenants/default/kafka/dead-letters/{dead_id}
| 文件生命周期 | 上传完成、附件绑定、群文件登记/删除、OSS 元数据和引用表。 |
| 工作台事件 | 会议、日报/月报、总经理信箱、红点/系统通知投递。 |
| 死信闭环 | 死信查询、忽略、解决、重放,以及重放后二次成功/失败。 |
| Outbox | Kafka 停止/恢复时 `im_kafka_outbox`、Admin outbox 的可靠投递。 |
| Kafka 生产失败 | Kafka 停止/恢复时,确认 IM 不写 `im_kafka_outbox`,接口/WS 返回发送失败,恢复后可重新发送。 |
| Admin 页面 | 操作单状态从 pending/running 到 succeeded/failed/dead 的页面联调。 |
### 10.2 2026-08-27 完整后端链路联调
+4
View File
@@ -0,0 +1,4 @@
-- Remove the deprecated Kafka DB outbox table from a tenant database.
-- Run once per tenant business database after deploying the direct Kafka publish path.
DROP TABLE IF EXISTS im_kafka_outbox;
-23
View File
@@ -29,29 +29,6 @@ CREATE TABLE IF NOT EXISTS im_kafka_consume_log (
INDEX idx_im_kafka_consume_log_topic(source_topic, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_kafka_outbox (
id BIGINT NOT NULL AUTO_INCREMENT,
event_id VARCHAR(96) NOT NULL,
topic VARCHAR(128) NOT NULL,
event_type VARCHAR(96) NOT NULL,
biz_type VARCHAR(64) NOT NULL DEFAULT '',
biz_id VARCHAR(128) NOT NULL DEFAULT '',
key_value VARCHAR(256) NOT NULL DEFAULT '',
envelope JSON NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3),
sent_at DATETIME(3),
last_reason VARCHAR(64) NOT NULL DEFAULT '',
last_error VARCHAR(1000) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_kafka_outbox_event(event_id),
INDEX idx_im_kafka_outbox_pending(status, next_retry_at, id),
INDEX idx_im_kafka_outbox_biz(biz_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_kafka_dead_letters (
id BIGINT NOT NULL AUTO_INCREMENT,
dead_id VARCHAR(96) NOT NULL,