lab/note/归档/zero-examples.md
张育新 be711334fb docs(note): 添加笔记目录及 gitignore 规则
- 新增 note 目录包含 AI、SQL、Golang、网络等多领域学习笔记
- 更新 .gitignore 添加 note/assets/ 和 note/项目/ 忽略规则
2026-07-10 09:24:40 +08:00

304 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# HTTP
## 上传文件
> **https://github.com/zeromicro/zero-examples/tree/main/http/upload**
### 前端
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<form
enctype="multipart/form-data"
action="http://localhost:8888/upload"
method="post"
>
<input type="file" name="myFile" />
<input type="submit" value="upload" />
</form>
</body>
</html>
```
> **参数说明**
- `enctype="multipart/form-data"` 用于支持文件的二进制传输
- `<input type="file">` 让用户挑选本地文件
- `action``method`,将数据以 `POST` 方式提交到后端地址
### 后端
```yaml
Name: upload-api
Host: 0.0.0.0
Port: 8888
MaxBytes: 1073741824
Timeout: 30000
```
> **yaml文件需要设置上传的最大字节并且如果为大文件需要考虑超时时间设置是否合理**
```go
func (l *UploadLogic) Upload(r *http.Request) (resp *types.Response, err error) {
err = r.ParseMultipartForm(10 << 27)
if err != nil {
fmt.Println("错误发生:", err)
return nil, err
}
file, handler, err := r.FormFile("myFile")
if err != nil {
fmt.Println(err)
return nil, err
}
defer file.Close()
fmt.Printf("Uploaded File: %+v\n", handler.Filename)
fmt.Printf("File Size: %+v\n", handler.Size)
fmt.Printf("MIME Header: %+v\n", handler.Header)
tempFile, err := os.Create(path.Join(".", handler.Filename))
if err != nil {
fmt.Println(err)
return nil, err
}
defer tempFile.Close()
io.Copy(tempFile, file)
return &types.Response{
OK: 0,
}, nil
}
```
> **API逻辑说明**
- 需要在handler里面将`*http.Request`传递到逻辑层
- `ParseMultipartForm`用于解析 `multipart/form-data` 类型的请求体
主要作用是:
解析表单字段(包括文件上传字段)
将请求体拆分成多个部分parts并填充到 `r.MultipartForm`
内存与磁盘分配:最多在内存中保留 `maxFileSize` 字节,超出的部分会写入临时文件
- `FormFile(key string)` 从已解析的 `r.MultipartForm` 中提取名为 `"myFile"` 的文件部分
- `tempFile, err := os.Create(path.Join(".", handler.Filename))`将文件上传到当前目录
# DTM
## 基础概念
- **APApplication**:应用程序,发起全局事务请求。
- **TMTransaction Manager**:事务协调者,负责协调分布式事务的执行流程。
- **RMResource Manager**:资源管理者,例如订单服务、库存服务等,执行具体业务与补偿逻辑
- **事务分支**:我们把每个服务管理的全局事务组成部分,称为事务分支,例如前面的转账,分为转出和转入两个事务分支
- **分支操作**每个事务分支在SAGA、XA、TCC等事务模式下会有多个操作例如转出事务分支包括正向操作TransOut和TransOutCompensate
- **本地事务**:转出事务分支中的正向操作,通常会开启一个事务,对余额进行扣减,我们将数据操作的这个事务,称为本地事务
- **GID**全局事务ID用于标记全局事务必须唯一。该ID可以采用UUID生成也可以使用业务上的ID例如订单ID等
| 模式 | 场景 | 典型使用 |
| ---------- | ---------------------------------------- | --------------------------------- |
| 二阶段消息 | 需要确保消息通知或缓存更新与本地事务同步 | 发短信/推送、消息投递、缓存一致性 |
| Saga | 流程复杂、长链路、不需强一致性 | 电商下单→扣库存→支付等微服务流程 |
| TCC | 强一致、高隔离资源场景 | 金融系统、库存冻结、大额支付 |
## 二阶段消息
是dtm首创的事务模式用于替换本地事务表和事务消息这两种现有的方案。它能够保证本地事务的提交和全局事务提交是“原子的”适合解决不需要回滚的分布式事务场景比如发消息、更新缓存等
- **准备阶段**Prepare ,应用先发一个准备信号给 DTM Server表示“我现在要做本地事务了”。DTM Server 接收到后先记录这个请求,但不触发后续通知。
- 执行本地事务, 应用开始执行本地事务(如写数据库、扣款),并在同一个数据库里,记录这次全局事务的 gid全局事务 ID。这保证了后续可以根据 gid 判断本地事务是否已成功。
- **提交阶段**Submit 本地事务成功后,应用再向 DTM 提交提交请求。DTM 执行这个操作后,才真正触发下游服务或消息投递。开始调用其他微服务或消息队列,实现完整的全局动作。
**优点:**
- 保证“本地事务 + 消息”业务要么同时成功,否则都不执行。(强触发保障)
- 对开发者透明,业务侵入小。
**缺点:**
- 需要开发一套 Prepare/Submit 调用,系统需定期监控、回查状态。
- 不适用于需要复杂补偿的场景。
## SAGA
核心思想是将长事务拆分为多个短事务由Saga事务协调器协调如果每个短事务都成功提交完成那么全局事务就正常完成如果某个步骤失败则根据相反顺序一次调用补偿操作。例如我们要进行一个类似于银行跨行转账的业务将A中的30元转给B根据Saga事务的原理我们将整个全局事务切分为以下服务
- **转出**TransOut服务这里转出将会进行操作A-30
- **转出补偿**TransOutCompensate服务回滚上面的转出操作即A+30
- **转入**TransIn服务转入将会进行B+30
- **转入补偿**TransInCompensate服务回滚上面的转入操作即B-30
**优点:**
- 流程自然、开发简单,只要为操作实现补偿逻辑即可。
- 对资源无锁设计,支持高并发、长流程,性能佳。
**缺点:**
- 是最终一致性,存在临时中间状态(如库存已扣但未支付)。
- 无事务隔离,可能出现脏写或并发冲突。
- 补偿逻辑可能复杂易错(幂等、防重复)。
## TCC
TCC 是一种“预留-确认-取消”的事务模式,将事务分为三个阶段,高精度控制点 的场景,比如:支付系统、资金冻结、库存预扣等
- **Try** 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
- **Confirm** 阶段如果所有分支的Try都成功了则走到Confirm阶段。Confirm真正执行业务不作任何业务检查只使用 Try 阶段预留的业务资源
- **Cancel** 阶段如果所有分支的Try有一个失败了则走到Cancel阶段。Cancel释放 Try 阶段预留的业务资源。
**优点:**
- 强一致性:通过 Try 预留资源Confirm/Cancel 明确执行或回滚,避免中间状态。
- 高并发友好,资源锁粒度小;性能优于传统 XA/2PC。
**缺点:**
- 业务侵入性高每个参与方需实现三个操作Try/Confirm/Cancel
- 实现复杂:需处理悬挂、空回滚、幂等、补偿重试等问题。
## SAGA实践
- 准备RM和DTM数据表
[GitHub地址](https://github.com/dtm-labs/dtm/tree/main/sqls)
- DTM配置
[GitHub地址](https://github.com/dtm-labs/dtm/blob/main/conf.sample.yml)
- docker部署参考配置
```yaml
LogLevel: 'info'
Server:
HttpPort: 36789 #HTTP port
GrpcPort: 36790 #Grpc port
Store:
Driver: "mysql" # 数据库类型
Host: "mysql" # 使用容器名
Port: 3306
User: "root"
Password: "root"
Db: "dtm"
MicroService:
Driver: "dtm-driver-gozero" #使用go-zero的注册服务发现驱动
Target: "etcd://etcd:2379/dtmservice" # 使用容器名当前dtm的server直接注册到微服务所在的etcd集群中使用
EndPoint: "dtm:36790" # 使用容器名集群中的微服务可以直接通过etcd获得此地址跟dtm交互
```
- go-zero的yaml参考配置
```yaml
#AP使用etcd的配置
Etcd:
Hosts:
- etcd:2379
Key: order.api
#RM使用etcd的配置
OrderRpc:
Etcd:
Hosts:
- etcd:2379
Key: order.rpc
#RM使用etcd的配置
StockRpc:
Etcd:
Hosts:
- etcd:2379
Key: stock.rpc
```
SAGA 事务编排使用步骤
```go
// dtm服务在etcd中的调用地址
var dtmServer = "etcd://localhost:2379/dtmservice"
// 从配置中通过BuildTarget获得对应RM的etcd调用地址
orderTarget, err := l.svcCtx.Config.OrderRpcConf.BuildTarget()
stockTarget, err := l.svcCtx.Config.StockRpcConf.BuildTarget()
// 构造对应的请求结构体,用于后面的本地事务的执行或者回滚补偿
createOrderReq := &order.CreateReq{UserId: req.UserId, GoodsId: req.GoodsId, Num: req.Num}
deductReq := &stock.DecuctReq{GoodsId: req.GoodsId, Num: req.Num}
// 使用dtmgrpc注册一个全局事务ID用于标记全局事务
gid := dtmgrpc.MustGenGid(dtmServer)
// 开启全局事务,并提交
// dtm需要从dtm服务器调用该方法所以不走强类型而是走动态的url: busiServer+"/trans.TransSvc/TransOut"
// "/trans.TransSvc/TransOut"对应的是grpc_pb.go文件中Invoke方法所使用的路径
saga := dtmgrpc.NewSagaGrpc(dtmServer, gid).
Add(
orderTarget+"/pb.order/create",
orderTarget+"/pb.order/createRollback",
createOrderReq,
).
Add(
stockTarget+"/pb.stock/deduct",
stockTarget+"/pb.stock/deductRollback",
deductReq,
)
err = saga.Submit()
```
服务实现(包含子事务屏障)
```go
// 从 gRPC 请求上下文中提取分布式事务信息,构建一个用于管理子事务屏障的对象
barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
// 获取一个sqlx的数据库连接
db, err := sqlx.NewMysql(l.svcCtx.Config.DB.DataSource).RawDB()
if err != nil {
// 一般数据库不会错误不需要dtm回滚就让他一直重试
// 返回为status.Error(codes.Internal, err.Error())会让dtm一直重试不会回滚直到
// 返回为status.Error(codes.Aborted, dtmcli.ResultFailure)会让dtm立刻调用回滚方法
return nil, status.Error(codes.Internal, err.Error())
}
// 开启本地事务(这里没写,正常应该在本地事务中再开启子事务屏障进行业务处理
// 将业务逻辑包裹在子事务屏障的保护下执行
if err := barrier.CallWithDB(db, func(tx *sql.Tx) error {
order := new(model.Order)
order.GoodsId = in.GoodsId
order.Num = in.Num
order.UserId = in.UserId
_, err = l.svcCtx.OrderModel.Insert(tx, order)
if err != nil {
return fmt.Errorf("创建订单失败 err : %v , order:%+v \n", err, order)
}
return nil
}); err != nil {
// 同上让dtm一直重试不回滚
return nil, status.Error(codes.Internal, err.Error())
}
// 补偿操作跟上面差不多,就是业务逻辑修改
```