# MySQL 事务 Outbox 适配包 ## 1. 用途与安装 业务写库与提交 Orbit 是两次独立操作。Outbox 将业务变更和待发请求放在同一个业务数据库事务中,再通过后台发送器提交任务。提交响应丢失时复用原操作身份,避免业务记录已保存而任务丢失。 ```sh go get git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql@v0.1.1 ``` 对应 Git 标签为 `adapters/outbox/mysql/v0.1.1`;业务项目固定模块版本 `v0.1.1`,无需本地 `replace`。本包要求 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)。