Files
sundynix-micro-be/docs/dtm-saga-distributed-transaction-plan.md
T
2026-05-24 23:04:09 +08:00

333 lines
12 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.
# DTM Saga 分布式事务改造 — 用户注册流程
## 背景
当前 [findorcreateuserbyopenidlogic.go](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/plant/rpc/internal/logic/findorcreateuserbyopenidlogic.go) 中,Plant RPC 直接跨服务边界操作 System 的 `sundynix_user` 表,使用本地事务一次性创建基础用户和扩展用户。这违反了微服务自治原则,**一旦分库部署将导致数据不一致**。
本方案引入 **DTM (Distributed Transaction Manager)****Saga 模式**,将跨服务的用户注册拆分为两个独立的子事务,由 DTM 协调保证强一致性。
---
## User Review Required
> [!IMPORTANT]
> **基础设施依赖**:本方案需要部署一个 DTM 服务实例(Docker 容器),并且 DTM 依赖 MySQL 存储事务状态。请确认你的 `192.168.100.127` 服务器上可以运行新的 Docker 容器。
> [!WARNING]
> **DTM 维护状态**`dtm-driver-gozero` 项目近两年更新较少,但核心功能稳定且被广泛使用。与 go-zero v1.10.1 兼容性需要验证,如果有兼容性问题我会做适配。
## Open Questions
> [!IMPORTANT]
> 1. **DTM 部署方式**:你希望 DTM 服务部署在 `192.168.100.127` 上(推荐,和 etcd/MySQL 同机器),还是本地开发环境?
> 2. **DTM 存储**DTM 需要一个 MySQL 数据库来存储事务状态。是新建一个 `dtm` 数据库,还是在现有的 `sundynix_micro_go` 中加表?推荐新建独立数据库。
> 3. **Saga vs TCC 选择**:对于用户注册场景,Saga 更合适(逻辑简单、不需要资源预留),TCC 适合需要"冻结"资源的场景(如扣款)。你确定要用 Saga 还是 TCC?下面默认按 **Saga** 设计。
---
## 架构设计
### 改造前 vs 改造后
```mermaid
graph TB
subgraph "改造前 ❌"
A1[Plant RPC] -->|"直接 tx.Create"| B1[sundynix_user 表]
A1 -->|"直接 tx.Create"| C1[plant_user_profile 表]
style B1 fill:#ff6b6b,color:#fff
end
subgraph "改造后 ✅"
DTM[DTM Server] -->|"Step 1: CreateUserByMini"| SYS[System RPC]
DTM -->|"Step 2: CreateProfile"| PLT[Plant RPC]
SYS -->|"本地事务"| B2[sundynix_user 表]
PLT -->|"本地事务"| C2[plant_user_profile 表]
DTM -.->|"失败补偿"| SYS2[System: CompensateCreateUser]
DTM -.->|"失败补偿"| PLT2[Plant: CompensateCreateProfile]
style DTM fill:#4ecdc4,color:#fff
end
```
### Saga 事务流程
```mermaid
sequenceDiagram
participant Plant as Plant RPC<br>(事务发起者)
participant DTM as DTM Server
participant Sys as System RPC
participant PlantSub as Plant RPC<br>(子事务)
Plant->>DTM: 提交 Saga 全局事务 (gid)
DTM->>Sys: Step1: CreateUserByMini(openId, clientId, nickName)
Sys->>Sys: barrier.Call → INSERT sundynix_user → 返回 userId
Sys-->>DTM: 返回 {userId}
DTM->>PlantSub: Step2: CreateProfile(userId, openId, sessionKey...)
PlantSub->>PlantSub: barrier.Call → INSERT plant_user_profile
PlantSub-->>DTM: 返回 success
DTM-->>Plant: Saga 完成
Note over DTM,PlantSub: 如果 Step2 失败:
DTM->>PlantSub: CompensateCreateProfile(userId)
DTM->>Sys: CompensateCreateUserByMini(userId)
```
---
## Proposed Changes
### 1. 基础设施 — DTM 服务部署
`192.168.100.127` 上通过 Docker 部署 DTM
```bash
docker run -d --name dtm \
-p 36789:36789 \
-p 36790:36790 \
-e STORE_DRIVER=mysql \
-e STORE_HOST=192.168.100.127 \
-e STORE_PORT=3307 \
-e STORE_USER=root \
-e STORE_PASSWORD=root \
-e MICRO_SERVICE_DRIVER=dtm-driver-gozero \
-e MICRO_SERVICE_TARGET=etcd://192.168.100.127:2379/dtmservice \
yedf/dtm:latest
```
同时需要在数据库中创建 `dtm_barrier` 表(DTM 子事务屏障所需),分别在 System 和 Plant 连接的数据库中建表:
```sql
CREATE TABLE IF NOT EXISTS dtm_barrier (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
trans_type VARCHAR(45) DEFAULT '',
gid VARCHAR(128) DEFAULT '',
branch_id VARCHAR(128) DEFAULT '',
op VARCHAR(45) DEFAULT '',
barrier_id VARCHAR(45) DEFAULT '',
reason VARCHAR(45) DEFAULT '',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_barrier (gid, branch_id, op, barrier_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
---
### 2. 依赖管理
#### [MODIFY] [go.mod](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/go.mod)
新增 DTM 相关依赖:
```
github.com/dtm-labs/dtmgrpc
github.com/dtm-labs/dtmdriver-gozero
```
---
### 3. System RPC — 新增子事务方法
#### [MODIFY] [system.proto](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/system/rpc/pb/system.proto)
新增消息和 RPC 方法:
```protobuf
// DTM Saga 子事务:小程序注册基础用户
message CreateUserByMiniReq {
string openId = 1; // 用于幂等判断
string clientId = 2;
string nickName = 3;
}
message CreateUserByMiniResp {
string userId = 1;
}
// Service 新增:
rpc CreateUserByMini(CreateUserByMiniReq) returns (CreateUserByMiniResp);
rpc CreateUserByMiniCompensate(CreateUserByMiniReq) returns (CommonResp);
```
#### [NEW] createUserByMiniLogic.go
- 使用 `dtmgrpc.BarrierFromGrpc(ctx)` 创建子事务屏障
- `barrier.CallWithDB(db, func(tx *sql.Tx) error { ... })` 内执行 INSERT
- 自动处理幂等、空补偿、悬挂问题
#### [NEW] createUserByMiniCompensateLogic.go
- 同样使用子事务屏障
- 补偿逻辑:`DELETE FROM sundynix_user WHERE id = ? AND client_id = ?`
---
### 4. Plant RPC — 新增子事务方法 + 重构编排
#### [MODIFY] [plant.proto](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/plant/rpc/pb/plant.proto)
新增消息和 RPC 方法:
```protobuf
// DTM Saga 子事务:创建 Plant 用户扩展表
message CreateProfileReq {
string userId = 1;
string openId = 2;
string sessionKey = 3;
string unionId = 4;
string saOpenId = 5;
string clientId = 6;
string nickName = 7;
}
message CreateProfileResp {
string profileId = 1;
}
// Service 新增:
rpc CreateProfile(CreateProfileReq) returns (CreateProfileResp);
rpc CreateProfileCompensate(CreateProfileReq) returns (CommonResp);
```
#### [NEW] createProfileLogic.go
- 子事务屏障内执行:查找初始等级 + INSERT `plant_user_profile`
#### [NEW] createProfileCompensateLogic.go
- 子事务屏障内执行:`DELETE FROM sundynix_plant_user_profile WHERE user_id = ?`
#### [MODIFY] [findorcreateuserbyopenidlogic.go](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/plant/rpc/internal/logic/findorcreateuserbyopenidlogic.go)
**核心改造**:从直接操作数据库改为 **DTM Saga 编排者**
```go
func (l *FindOrCreateUserByOpenIdLogic) FindOrCreateUserByOpenId(in *plant.FindOrCreateUserByOpenIdReq) (*plant.PlantUserProfile, error) {
// 1. 先查询是否已存在(不变)
var profile plantModel.UserProfile
err := l.svcCtx.DB.Where("mini_open_id = ?", in.OpenId).First(&profile).Error
if err == nil {
// 已存在:更新 session_key,直接返回(不变)
...
return &plant.PlantUserProfile{...}, nil
}
// 2. 新用户 → 发起 DTM Saga 分布式事务
dtmServer := "etcd://192.168.100.127:2379/dtmservice"
systemTarget := "etcd://192.168.100.127:2379/system.rpc"
plantTarget := "etcd://192.168.100.127:2379/plant.rpc"
gid := dtmgrpc.MustGenGid(dtmServer)
sysReq := &sysPb.CreateUserByMiniReq{
OpenId: in.OpenId,
ClientId: in.ClientId,
NickName: "园艺新手",
}
plantReq := &plantPb.CreateProfileReq{
UserId: "", // 由 DTM 传递(见下方说明)
OpenId: in.OpenId,
SessionKey: in.SessionKey,
UnionId: in.UnionId,
SaOpenId: in.SaOpenId,
ClientId: in.ClientId,
NickName: "园艺新手",
}
saga := dtmgrpc.NewSagaGrpc(dtmServer, gid).
Add(systemTarget+"/system.SystemService/CreateUserByMini",
systemTarget+"/system.SystemService/CreateUserByMiniCompensate",
sysReq).
Add(plantTarget+"/plant.PlantService/CreateProfile",
plantTarget+"/plant.PlantService/CreateProfileCompensate",
plantReq)
saga.WaitResult = true // 等待事务结果
err = saga.Submit()
if err != nil {
return nil, fmt.Errorf("注册用户失败: %w", err)
}
// 3. 事务完成后查询完整 profile
l.svcCtx.DB.Where("mini_open_id = ?", in.OpenId).First(&profile)
return &plant.PlantUserProfile{...}, nil
}
```
> [!IMPORTANT]
> **关于 userId 传递问题**:Saga 模式下各步骤是独立的,Step2 无法直接获取 Step1 返回的 userId。解决方案是:**CreateProfile 内部根据 openId 查询刚创建的 sundynix_user 获取 userId**(两步在同一个 gid 内是顺序执行的,Step1 完成后 Step2 才会执行)。或者使用 DTM 的 **Workflow 模式** 代替 Saga,支持步骤间传值。
---
### 5. 配置更新
#### [MODIFY] [plant.yaml](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/plant/rpc/etc/plant.yaml)
```yaml
# 新增 DTM 和 System RPC 配置
DtmServer: "etcd://192.168.100.127:2379/dtmservice"
SystemRpc:
Etcd:
Hosts:
- 192.168.100.127:2379
Key: system.rpc
```
#### [MODIFY] Plant RPC config.go, serviceContext.go
- Config 增加 `DtmServer string``SystemRpc zrpc.RpcClientConf`
- ServiceContext 增加 SystemRpc 客户端(仅用于非 DTM 场景的备用查询)
---
### 6. 删除旧的跨服务代码
#### [MODIFY] [findorcreateuserbyopenidlogic.go](file:///Users/zhangjianmin/sourceCode/GolandProjects/src/sundynix-micro-go/app/plant/rpc/internal/logic/findorcreateuserbyopenidlogic.go)
- 移除对 `sysModel.SundynixUser` 的直接引用和 import
- 移除 `tx.Create(&sysUser)` 这段跨服务写入代码
---
## 文件变更总结
| 服务 | 文件 | 操作 | 说明 |
|---|---|---|---|
| **根目录** | `go.mod` | MODIFY | 添加 dtmgrpc、dtm-driver-gozero 依赖 |
| **System RPC** | `system.proto` | MODIFY | 新增 CreateUserByMini + Compensate RPC |
| **System RPC** | `createUserByMiniLogic.go` | NEW | 子事务:创建基础用户 (barrier) |
| **System RPC** | `createUserByMiniCompensateLogic.go` | NEW | 补偿:删除基础用户 (barrier) |
| **Plant RPC** | `plant.proto` | MODIFY | 新增 CreateProfile + Compensate RPC |
| **Plant RPC** | `createProfileLogic.go` | NEW | 子事务:创建扩展用户 (barrier) |
| **Plant RPC** | `createProfileCompensateLogic.go` | NEW | 补偿:删除扩展用户 (barrier) |
| **Plant RPC** | `findorcreateuserbyopenidlogic.go` | MODIFY | 重构为 Saga 编排者 |
| **Plant RPC** | `config.go` | MODIFY | 添加 DtmServer 配置 |
| **Plant RPC** | `serviceContext.go` | MODIFY | 初始化 DTM driver |
| **Plant RPC** | `plant.yaml` | MODIFY | 添加 DTM 连接配置 |
| **Database** | SQL | EXECUTE | 创建 dtm_barrier 屏障表 |
---
## Verification Plan
### Automated Tests
```bash
# 1. 编译验证
go build ./app/system/rpc/...
go build ./app/plant/rpc/...
go build ./app/auth/api/...
# 2. 确认 DTM 服务健康
curl http://192.168.100.127:36789/api/dtmsvr/version
```
### Manual Verification
1. 启动 DTM + System RPC + Plant RPC + Auth API
2. 小程序端发起 miniLogin → 验证新用户同时出现在 `sundynix_user``plant_user_profile`
3. 模拟 Plant CreateProfile 失败 → 验证 System 的 CompensateCreateUserByMini 被调用,`sundynix_user` 中无孤儿记录
4. 通过 DTM 管理台 (`http://192.168.100.127:36789`) 查看事务状态