Files
orbit/docs/sdk/outbox-mysql.md

124 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MySQL 事务 Outbox 适配包
## 1. 用途与安装
业务写库与提交 Orbit 是两次独立操作。Outbox 将业务变更和待发请求放在同一个业务数据库事务中,再通过后台发送器提交任务。提交响应丢失时复用原操作身份,避免业务记录已保存而任务丢失。
```sh
go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@codex/orbit-foundation
```
Go 会将分支解析为固定版本写入 go.mod。上线应固定已验证的版本,不在每次部署时追踪分支。本包要求 Go 1.27.1,使用 SDK v0.1.0 和 MySQL 8.4 验证;不向核心 SDK 加入 SQL/ORM 依赖,也不依赖 Orbit 服务端模型。其他数据库和 ORM 专用适配尚未提供。
```go
import (
"context"
"database/sql"
"time"
outbox "git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
```
完整可编译工程在 [HTTP 服务示例](../../examples/http-service/README.md);[旧 CLI 示例](../examples/task-outbox.md)现在也调用本包,不维护第二份发送实现。
## 2. 数据库与事务约束
在**业务数据库**应用 [schema.sql](../../adapters/outbox/mysql/schema.sql)。表名保留 `sender_outbox`,兼容原示例已经初始化的表。中文模型见 [Record](../../adapters/outbox/mysql/model.go)。没有外键,不执行数据删除,不属于 Orbit 平台迁移。
- `New` 不建表、不连接网络、不接管连接池关闭;显式 `Init` 只创建新表,遇到已有表报错。
- 生产由业务迁移工具管理 `SchemaSQL()` 的内容;不要在每次启动时执行 Init。
- `db` 和传入 `tx` 必须来自同一个业务库、同一个主写端;不支持跨库事务或读副本确认。
- 自行创建连接池时配置 `parseTime=true`、UTC 会话时区和合理连接/读写超时;`Open(ctx, dsn)` 是可选的已配置连接池入口。
- 事务锁序统一为 **Outbox → 业务行**。先调用 EnqueueTx,再写业务行;受理回调遵守同样顺序。不要持有业务行锁反向等待 Outbox。
- 业务操作 ID 必须由调用方在首次请求前生成并保存,格式为 32 位小写十六进制;同命名空间唯一。HTTP 调用方丢响应后使用原 ID 与原内容重试。
## 3. 公共方法
| 方法 | 职责 |
| --- | --- |
| `Open(ctx, dsn) (*sql.DB, error)` | 可选的业务库连接池构造;不会自动建表 |
| `SchemaSQL() string` | 返回初始表结构供业务迁移管理 |
| `Init(ctx, db) error` | 显式创建新表,仅用于首次初始化 |
| `New(db, namespaceID, queueID, Options) (*Store, error)` | 固定目标及可选受理回调,可并发复用 |
| `EnqueueTx(ctx, tx, operationID, SubmitTaskRequest) (duplicate, error)` | 在调用方事务写入待发任务;不 Commit/Rollback、不发 HTTP |
| `SendOnce(ctx, client) (Record, error)` | 有界领取并处理一条到期记录;无到期项返回 ErrIdle |
| `Run(ctx, client, RunOptions) error` | 持续发送;取消后停止领取,限时收尾当前发送 |
| `Status(ctx, operationID) (Record, error)` | 只读固定命名空间及队列中的本地提交状态 |
基本构造不要求业务回调:
```go
sender, err := outbox.New(db, namespaceID, queueID, outbox.Options{})
if err != nil { return err }
```
`Options.OnAccepted` 类型为:
```go
type AcceptedHook func(context.Context, *sql.Tx, Record, orbit.SubmitTaskResult) error
```
回调与 Outbox 的 ACCEPTED 更新在同一个短事务内执行,并核对最新领取代次。可在此将平台 TaskID 关联到业务记录。回调必须幂等、响应 context,只做同库 SQL,不能 Commit/Rollback 或调用外部服务;失败整笔确认回滚,返回 `ErrAcceptance`。回调收到的 Record 仍是被锁定的 SENDING 记录,受理 TaskID 从 receipt 读取。不开启回调时,任务关联保存在 Outbox 自身,业务可按 OperationID 查询。
## 4. 加入现有业务事务
下面是接入函数片段。完整业务重复核对与错误处理见 [jobs/store.go](../../examples/http-service/internal/jobs/store.go)。
```go
func save(ctx context.Context, db *sql.DB, sender *outbox.Store,
operationID string, request orbit.SubmitTaskRequest) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil { return err }
defer tx.Rollback()
duplicate, err := sender.EnqueueTx(ctx, tx, operationID, request)
if err != nil { return err } // 必须回滚整笔事务
if !duplicate {
// 在这里使用 tx 写入自己的业务表。
// 每一个 SQL 错误都必须返回;禁止忽略错误后提交。
} else {
// 核对原业务记录与本次输入一致,不能再次执行扣款、计数等操作。
}
if err := tx.Commit(); err != nil {
return outbox.ErrCommitUnconfirmed
}
return nil
}
```
EnqueueTx 返回 nil 只代表该事务内写入/核对完成。事务提交前另一发送器看不到该记录;调用方回滚会同时撤销待发任务与业务写入。同一事务重复入队返回 duplicate=true,但仍需由调用方确认 Commit。默认 REPEATABLE READ 下的重复核对使用共享锁当前读,不依赖旧快照,也不升级重复 INSERT 持有的共享锁。
COMMIT 错误不能解释成“肯定没保存”。保留原 ID 与内容,重做同一业务事务核对;不得生成新 ID 补发。冻结请求按字节摘要核对,目标、提交键、默认值和数字精度保持不变。
## 5. 发送器与生命周期
```go
err := sender.Run(workerContext, client, outbox.RunOptions{
PollInterval: 500 * time.Millisecond,
ShutdownTimeout: 20 * time.Second,
})
```
RunOptions 默认轮询间隔 500ms(10ms–10s),关闭期限默认 20s(1ms–5min)。HTTP 在数据库事务外,单次网络阶段最多 15s、发送请求最多 10s;领取租约 30s,SQL 确认最多 5s。数据库时间控制首次入库后 24h、最多 100 次领取的总预算;进程重启不重置预算。
Run context 取消后停止新领取,在关闭期限内等待当前交换与本地确认,宿主必须等待 Run 返回。`ErrShutdownTimeout` 表示有未核对的持久记录,应在重启后沿原身份恢复。HTTP 暂时失败在库内退避;数据库错误或受理回调失败会让 Run 返回错误,由宿主降低就绪状态、关闭或按自身策略恢复。REVIEW 记录停止自动发送,需要监控 Status/数据库并人工核查。
允许多个 Run/进程共用同一范围,由 MySQL SKIP LOCKED 和递增代次协调。网络请求可能重复,平台使用原幂等键去重;不承诺业务外部效果恰好一次。
## 6. 状态与错误
| 状态/错误 | 含义及处理 |
| --- | --- |
| PENDING / SENDING | 未完成本地受理确认;不能据此认定 Orbit 没收到 |
| ACCEPTED | 已确认平台受理,TaskID/ReceiptJSON 可用;不是执行成功 |
| REVIEW | 权限、内容冲突、无效回执或预算耗尽;保留证据人工核对 |
| ErrConflict | 原操作的目标或内容不一致,不能覆盖 |
| ErrCommitUnconfirmed | 领取/确认 COMMIT 回执不明;保留原身份核对 |
| ErrLeaseLost | 领取代次已替换,旧发送器不得覆盖新结果;Run 可继续其他项 |
| ErrAcceptance | 本地受理关联回调失败,确认事务未获成功确认 |
| ErrStorage | 存储失败;当前进度以持久记录为准 |
| ErrIdle / ErrNotFound | 无到期记录 / 当前范围没有该操作 |
本版本只支持 SubmitTask,不提供流程启动/信号 Outbox 命令,不自动删除历史、重置发送预算或解除 REVIEW。任务完成另用可靠订阅与业务 Inbox,详见 [完整 HTTP 示例](../examples/http-service.md)。