惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

W
WeLiveSecurity
The Last Watchdog
The Last Watchdog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
G
Google Developers Blog
博客园 - 叶小钗
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 三生石上(FineUI控件)
Help Net Security
Help Net Security
Cloudbric
Cloudbric
AI
AI
N
News | PayPal Newsroom
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
SecWiki News
SecWiki News
H
Heimdal Security Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Proofpoint News Feed
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
V
V2EX
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
S
Security Affairs
L
LangChain Blog
The Hacker News
The Hacker News
F
Full Disclosure
aimingoo的专栏
aimingoo的专栏
Hacker News - Newest:
Hacker News - Newest: "LLM"
腾讯CDC
Webroot Blog
Webroot Blog
A
About on SuperTechFans
H
Hacker News: Front Page
Cyberwarzone
Cyberwarzone
WordPress大学
WordPress大学
L
LINUX DO - 热门话题
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
M
MIT News - Artificial intelligence

博客园 - 干炸小黄鱼

timex 处理时间戳 gorm-gen go 雪花算法 golang每日一库--协程池库ants golang每日一库--json解析库gjson python高级编程-asyncio python高级编程-condition python高级编程-event python装饰器-自动重试 EAP系统 go实现实现 SECS/GEM 协议 设备通信协议 SECS go项目使用Jenkins进行CICD go操作ES mongo db聚合查询 go如何使用mongodb Apache ShardingSphere paxos and raft (分布式一致性算法) go使用zookeeper分布式锁以及和redis差异 go使用 seata 示例 Alibaba 分布式事务 Seata go中使用saga 分布式事务TCC 熔断器 Hystrix OR Sentinel k8s下部署consul and etcd Consul OR Etcd 【力扣hot100】双指针-盛水最多的容器 【力扣hot100】滑动窗口-最小覆盖子串 shell脚本合集 分布式id生成器 springboot通用CURD Python PB级检索系统架构设计 rancher 在三台机器搭建k8s集群 python ssh clinet 数据库排序Null值字段靠后/靠前 常规web项目 docker-compose 例子 手搓一个验证码 使用itertools 中的groupby 对字典数组进行分组后排序 使用开源库 geoip2 获取某ip的经纬度地理信息 python中 apscheduler.schedulers.blocking.BlockingScheduler 定时执行任务 简单的python web项目的docker-compose.yml 示例 python和sliver交互 golang sliver二次开发自定义命令(格式乱后面再调) pydantic做参数校验 基于rancher部署k8s 地理位置相关基础数据 flask migrate时报错 Can't locate revision identified by '3d80e4c025df'
go中使用TCC示例
干炸小黄鱼 · 2026-04-21 · via 博客园 - 干炸小黄鱼

在 Go 微服务中实现 TCC,通常不会从零开始写所有逻辑,而是使用成熟的分布式事务框架。

目前 Go 社区最流行、功能最完善的 TCC 框架是 dtm (Distributed Transaction Manager)。它支持 Go、Python、Java 等多种语言,并且非常轻量。

下面我将基于 dtm 框架,为你演示如何在 Go 微服务中实现一个经典的 跨服务转账 场景(用户 A 扣款,用户 B 加款)。


🛠️ 核心概念与架构

在 Go 中使用 TCC,主要涉及三个角色:

  1. dtm 服务端 (Server):负责协调事务状态(Try/Confirm/Cancel)。
  2. 业务微服务 (Microservice):实现具体的 Try/Confirm/Cancel 逻辑。
  3. 客户端 (Client):发起分布式事务。

🚀 第一步:环境准备

假设你已经部署了 dtm 服务端(通常运行在 localhost:36789)。
你需要安装 Go 客户端 SDK:

go get github.com/dtm-labs/dtmcli

💻 第二步:实现业务微服务 (TCC 逻辑)

我们需要编写一个 HTTP 服务,暴露出 TCC 所需的三个接口:TryConfirmCancel

场景:账户服务,处理扣款。

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/dtm-labs/dtmcli"
	"github.com/gin-gonic/gin"
)

// 模拟数据库操作
func updateAccountBalance(uid string, amount int) error {
	// 这里应该连接真实的数据库 (MySQL/PostgreSQL)
	// 1. 开启本地事务
	// 2. 执行 SQL: UPDATE accounts SET balance = balance + ? WHERE uid = ?
	// 3. 提交事务
	fmt.Printf("数据库操作: 用户 %s 余额变动 %d\n", uid, amount)
	return nil
}

// 1. Try 阶段:尝试扣款(冻结资源)
func tccTry(c *gin.Context) {
	var req struct {
		UID    string `json:"uid"`
		Amount int    `json:"amount"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 业务逻辑:检查余额是否充足,如果充足则冻结金额
	// 注意:这里通常需要记录一个“冻结”状态,或者使用数据库的乐观锁
	fmt.Printf("Try: 准备冻结用户 %s 的金额 %d\n", req.UID, req.Amount)

	// 如果检查失败(例如余额不足),返回错误,dtm 会收到失败并触发 Cancel
	if req.Amount > 10000 { // 模拟余额不足
		c.JSON(400, gin.H{"message": "余额不足"})
		return
	}

	// 成功则返回 200
	c.JSON(200, gin.H{"message": "Try success"})
}

// 2. Confirm 阶段:确认扣款(正式执行)
func tccConfirm(c *gin.Context) {
	var req struct {
		UID    string `json:"uid"`
		Amount int    `json:"amount"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 业务逻辑:将 Try 阶段冻结的金额正式扣除
	fmt.Printf("Confirm: 正式扣除用户 %s 的金额 %d\n", req.UID, req.Amount)
	
	// 必须实现幂等性:如果之前已经扣过了,直接返回成功
	c.JSON(200, gin.H{"message": "Confirm success"})
}

// 3. Cancel 阶段:取消操作(回滚/解冻)
func tccCancel(c *gin.Context) {
	var req struct {
		UID    string `json:"uid"`
		Amount int    `json:"amount"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}

	// 业务逻辑:解冻 Try 阶段冻结的金额
	fmt.Printf("Cancel: 解冻用户 %s 的金额 %d\n", req.UID, req.Amount)

	// 必须实现幂等性
	c.JSON(200, gin.H{"message": "Cancel success"})
}

func main() {
	r := gin.Default()

	// 注册 TCC 接口
	// 实际生产中,这些接口通常通过 dtm 的屏障或者专门的路由暴露
	r.POST("/api/account/try", tccTry)
	r.POST("/api/account/confirm", tccConfirm)
	r.POST("/api/account/cancel", tccCancel)

	log.Println("Account Service starting on :8080")
	r.Run(":8080")
}

📡 第三步:发起分布式事务 (Client 端)

在另一个服务(比如订单服务或 API 网关)中,调用 dtm 来管理整个流程。

package main

import (
	"errors"
	"fmt"

	"github.com/dtm-labs/dtmcli"
)

const (
	// dtm 服务器地址
	DtmServer = "http://localhost:36789/dtmsvr"
	// 业务微服务地址
	AccountService = "http://localhost:8080/api/account"
)

func main() {
	// 1. 创建一个 TCC 事务对象
	// gid 是全局事务ID,必须全局唯一,通常使用 UUID
	gid := "your-unique-gid-12345"
	tcc, err := dtmcli.NewTccGid(DtmServer, gid)
	if err != nil {
		fmt.Println("创建事务失败:", err)
		return
	}

	// 2. 定义 Try/Confirm/Cancel 的请求数据
	reqData := map[string]interface{}{
		"uid":    "user_A",
		"amount": 100,
	}

	// 3. 添加 TCC 分支
	// 这里告诉 dtm:如果有子事务,请依次调用这些 URL
	err = tcc.AddBranches(
		// 第一个分支:用户 A 扣款
		[]string{
			AccountService + "/try",      // Try URL
			AccountService + "/confirm",  // Confirm URL
			AccountService + "/cancel",   // Cancel URL
		},
		reqData,
	)
	
	// 可以添加更多分支,例如用户 B 加款
	// tcc.AddBranches(...)

	if err != nil {
		fmt.Println("添加分支失败:", err)
		return
	}

	// 4. 提交事务
	// 这一步是同步阻塞的(也可以异步),dtm 会负责协调 Try -> Confirm/Cancel
	err = tcc.DoAndSubmit()

	if err != nil {
		// 事务失败(Try 阶段有报错,或者网络超时等)
		fmt.Println("分布式事务执行失败:", err)
	} else {
		// 事务成功(所有 Try 成功,且 Confirm 成功)
		fmt.Println("分布式事务执行成功!")
	}
}

⚠️ 关键难点与解决方案

在 Go 中手写 TCC 时,必须处理以下三个“坑”,dtm 框架内部通过 子事务屏障 很好地解决了这些问题,但你需要理解原理:

幂等性

  • 问题:网络抖动导致 Confirm 被调用了两次。
  • 解决:在数据库中记录事务状态。
    • 在执行 Confirm 前,检查 gid + branch_id 是否已经执行过。
    • 如果已执行,直接返回成功;如果未执行,执行逻辑并记录状态。

空回滚

  • 问题Try 请求因为网络原因没到达服务,但 dtm 超时了,直接调用了 Cancel。此时 Cancel 收到请求,但数据库里没有 Try 的记录。
  • 解决Cancel 逻辑需要判断:如果没有 Try 的记录,也要记录一条“已回滚”的日志,防止后续 Try 请求迟到后无法处理。

业务悬挂

  • 问题Cancel 先于 Try 到达(同上),执行了空回滚。然后 Try 请求迟到了。
  • 解决Try 执行时,必须检查是否存在“已回滚”的记录。如果存在,则拒绝执行 Try,直接返回失败。

📌 总结

在 Go 微服务中使用 TCC 的最佳实践:

  1. 引入 dtm:不要自己写协调器,使用 dtm 作为事务管理器。
  2. 业务改造:将业务逻辑拆分为 Try(检查/预留)、Confirm(执行)、Cancel(释放)三个 HTTP 接口。
  3. 使用屏障:利用 dtmcli 提供的屏障功能(dtmcli.Barrier)来处理数据库层面的幂等和空回滚,避免手写复杂的 SQL 逻辑。
  4. 数据库设计:业务表中通常需要增加状态字段(如 status: frozen/confirmed)来支持 Try 阶段的资源预留。

TCC的Confirm和Cancel如何实现幂等性

在 TCC 模式中,Confirm 和 Cancel 操作的幂等性至关重要。由于网络超时、重试机制或消息重复投递,这两个阶段的方法极有可能被重复调用。如果没有幂等性保障,就会导致资金重复扣减、库存重复释放等严重的数据不一致问题。

实现幂等性的核心思想是:确保同一个事务分支的同一阶段操作,无论被调用多少次,最终的业务状态和结果都是一致的

以下是两种主流的实现方案:

📝 方案一:基于数据库状态机(推荐)

这是最可靠、最常用的方案。它通过持久化的状态记录来控制操作的执行,能有效应对各种并发和异常场景。

核心步骤:

  1. 创建事务日志表:创建一张表(如 tcc_transaction_log),用于记录每个全局事务(XID)在每个阶段(Try/Confirm/Cancel)的执行状态。
  2. 利用唯一索引:在表中为 (xid, stage) 字段创建唯一索引。这是防止并发重复执行的关键。
  3. 执行前检查与状态更新:在执行 Confirm 或 Cancel 的具体业务逻辑前,先查询日志表。
    • 如果发现当前阶段(如 CONFIRM)的记录已存在且状态为成功,则直接返回成功,不再执行业务逻辑。
    • 如果记录不存在,则先执行业务逻辑,然后在同一个数据库事务中插入一条成功记录。

代码示例 (Go + GORM):

// TccTxLog 对应数据库表 tcc_transaction_log
type TccTxLog struct {
	ID    uint   `gorm:"primaryKey"`
	Xid   string `gorm:"uniqueIndex:idx_xid_stage;not null"` // 全局事务ID
	Stage string `gorm:"uniqueIndex:idx_xid_stage;not null"` // 阶段: TRY, CONFIRM, CANCEL
}

// Confirm 阶段实现幂等性
func (s *StockService) Confirm(xid string) error {
	// 1. 开启一个数据库事务
	tx := s.db.Begin()
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		}
	}()

	// 2. 检查是否已执行过 Confirm
	var log TccTxLog
	err := tx.Where("xid = ? AND stage = ?", xid, "CONFIRM").First(&log).Error
	if err == nil {
		// 记录存在,说明 Confirm 已执行,直接提交事务并返回成功
		tx.Commit()
		return nil
	}

	// 3. 记录不存在,执行真正的业务逻辑 (例如:扣减冻结库存)
	// ... 执行具体的数据库更新操作 ...
	// err = tx.Exec("UPDATE stock SET ... WHERE ...").Error
	// if err != nil { tx.Rollback(); return err }

	// 4. 业务逻辑成功后,插入一条 Confirm 执行记录
	log = TccTxLog{Xid: xid, Stage: "CONFIRM"}
	err = tx.Create(&log).Error
	if err != nil {
		tx.Rollback()
		return err // 插入失败,可能是唯一索引冲突,说明有其他请求抢先执行了
	}

	// 5. 提交事务
	return tx.Commit().Error
}

Cancel 阶段的实现逻辑与 Confirm 完全相同,只需将 stage 参数改为 "CANCEL" 即可。

⚡ 方案二:基于分布式锁(如 Redis)

这种方案通过分布式锁来保证同一时刻只有一个请求能执行 Confirm 或 Cancel 操作,从而实现幂等。

核心步骤:

  1. 获取锁:在执行操作前,使用全局事务ID(XID)和阶段(如 confirm:{xid})作为 Key,尝试从 Redis 获取一个分布式锁。
  2. 执行业务:如果成功获取锁,则执行业务逻辑。
  3. 释放锁:业务执行完成后,释放锁。

潜在问题:

  • 锁过期:如果业务执行时间过长,超过了锁的过期时间,可能导致锁被提前释放,另一个重试的请求会再次执行,破坏幂等性。
  • 状态丢失:单纯的锁只能防止并发,无法记录最终状态。服务重启后,锁信息丢失,无法判断之前是否执行过。

因此,通常不推荐单独使用分布式锁来实现幂等,它更适合作为数据库方案的补充,用于在高并发下减少数据库压力。

📌 方案对比与总结

方案 优点 缺点 适用场景
数据库状态机 可靠性高,状态持久化,能处理各种异常 对数据库有一定压力 生产环境首选,对数据一致性要求高的核心业务
分布式锁 性能好,响应快 实现复杂,存在锁过期风险,状态不持久 可作为数据库方案的缓存层,或用于非核心业务

总结

实现 TCC 幂等性的关键在于“先检查,后执行”。通过持久化的方式(如数据库唯一索引)来记录操作的执行状态,是保证最终一致性的最可靠手段。在实际开发中,像 dtm 这样的成熟框架已经内置了“子事务屏障”机制,自动帮你处理了幂等、空回滚和悬挂等问题,你只需要关注业务逻辑本身即可。