feat: minilogin改造
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
# 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`) 查看事务状态
|
||||
Reference in New Issue
Block a user