# 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`) 查看事务状态