# 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
(事务发起者)
participant DTM as DTM Server
participant Sys as System RPC
participant PlantSub as Plant RPC
(子事务)
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`) 查看事务状态