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

12 KiB
Raw Blame History

DTM Saga 分布式事务改造 — 用户注册流程

背景

当前 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 改造后

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 事务流程

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

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 连接的数据库中建表:

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

新增 DTM 相关依赖:

github.com/dtm-labs/dtmgrpc
github.com/dtm-labs/dtmdriver-gozero

3. System RPC — 新增子事务方法

[MODIFY] system.proto

新增消息和 RPC 方法:

// 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

新增消息和 RPC 方法:

// 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

核心改造:从直接操作数据库改为 DTM Saga 编排者

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

# 新增 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 stringSystemRpc zrpc.RpcClientConf
  • ServiceContext 增加 SystemRpc 客户端(仅用于非 DTM 场景的备用查询)

6. 删除旧的跨服务代码

[MODIFY] 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

# 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_userplant_user_profile
  3. 模拟 Plant CreateProfile 失败 → 验证 System 的 CompensateCreateUserByMini 被调用,sundynix_user 中无孤儿记录
  4. 通过 DTM 管理台 (http://192.168.100.127:36789) 查看事务状态