写在前面
前段时间,有同学反馈的一个面试问题,觉得分布式事务有点偏了,但其实也不算偏了,在java领域就有很火很成熟的seata分布式事务框架
(阿里和蚂蚁的同学主导,目前在apache孵化)。
之前我们讲过了两阶段提交、三阶段提交(感兴趣可以翻翻历史文章),这篇文章我们就来讲讲TCC分布式事务
两提交阶段的变种,并用go语言简单实现一个TCC。详细代码在 https://github.com/CocaineCong/BiliBili-Code
中
原理介绍
TCC 分别代表:
- Try:主要是对
业务系统做检测及资源预留
。 - Confirm:主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,
默认 Confirm 阶段是不会出错的
。 - Cancel:主要是在业务执行错误,需要
回滚的状态下执行的业务取消,释放预留资源
。
Try阶段完成业务的准备工作,Confirm阶段完成业务的提交,Cancel阶段完成事务的回滚。 基本原理如下图所示。

- 事务开始时,上层业务应用会向事务协调器注册启动事务。
- 业务应用会调用所有服务的Try接口,
完成一阶段准备
。 - 接着业务应用调用事务协调器进行提交或者回滚事务。
- 事务协调器会根据Try接口返回情况,
决定调用Confirm或者Cancel。如果接口调用失败,会进行重试
。
举个例子
我们就举一个电商平台的业务作为例子,当我们下单的时候,Try阶段需要检查以下:
- 检查当前账户的
金额是否充足,并预留对应的金额来扣除
。 - 检查当前商品
库存是否正常,并预留对应的库存
。 - 检查当前商品和当前账户的
订单能否创建成功
。
当我们Try阶段都执行成功之后,注意我们只是Try,做尝试,而不是真正的执行
,就开始执行Confirm阶段:
- 减去对应的余额
- 减去库存
- 创建订单
但如果我们Try阶段没成功,比如库存不足,或者余额不足,就开始执行Cancel阶段,因为我们Try虽然没有真正的执行,但是还是会预留预留一些资源空间出来的,我们需要将这些资源空间回滚。
- 回滚预留的余额
- 回滚库存
- 创建订单
我们可以看到这个TCC分布式事务其实是让应用自己定义数据库操作的粒度,使得降低锁冲突和锁的粒度,提高吞吐量。
当然我们也可以看到TCC方案的不足之处:
- 对应用的侵入性强:业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
- 实现难度较大且复杂:
需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略
。 - 为了满足一致性的要求,
Confirm和Cancel接口必须实现幂等
。
代码实现

我们用Go语言简单实现一下TCC吧,例子就用我们上面举的。
- Participant:定义TCC的三个接口,Try、Confirm、Cancel
go
type Participant interface {
Try(ctx context.Context) error
Confirm(ctx context.Context) error
Cancel(ctx context.Context) error
}
- Coordinator:定义TCC事务协调者
go
type Coordinator struct {
participants []Participant
}
func NewCoordinator(participants ...Participant) *Coordinator {
return &Coordinator{
participants: participants,
}
}
- 执行TCC事务:Try阶段,全部执行,如果有错误,再全部取消(这里我实现比较暴力,感兴趣的同学可以提pr优化)
go
var tryErr error
for _, p := range c.participants {
if err := p.Try(ctx); err != nil {
// Try阶段失败,执行Cancel
tryErr = err
}
}
if tryErr != nil {
c.cancelAll(ctx)
return tryErr
}
- 如果Try阶段有error,执行CancelAll,执行所有参与者的Cancel操作
go
func (c *Coordinator) cancelAll(ctx context.Context) {
var wg sync.WaitGroup
errChan := make(chan error, len(c.participants))
for _, p := range c.participants {
wg.Add(1)
go func(p Participant) {
defer wg.Done()
if err := p.Cancel(ctx); err != nil {
errChan <- fmt.Errorf("cancel failed: %w", err)
}
}(p)
}
wg.Wait() // 等待所有Cancel操作完成
close(errChan)
// ...
}
- 如果Try执行正常,则进行Confirm
go
var confirmErr error
for _, p := range c.participants {
if err := p.Confirm(ctx); err != nil {
confirmErr = fmt.Errorf("confirm phase failed: %w", err)
break
}
}
接着我们举个account service的例子来实现TCC的Try、Confirm、Cancel接口:

- account 信息服务定义:账户服务
go
type AccountService struct {
accountID string // 账号
amount float64 // 总金额
frozen float64 // 预留的金额
deductAmount float64 // 添加要扣减的金额字段
}
func NewAccountService(accountID string, balance float64) *AccountService {
return &AccountService{
accountID: accountID,
amount: balance,
}
}
- Try 函数实现 & 预留资源
go
func (a *AccountService) PrepareTry(amount float64) {
a.deductAmount = amount // 预留出应该需要多少钱
}
func (a *AccountService) Try(ctx context.Context) error {
a.frozen = a.deductAmount // 冻结预留的金额
a.amount -= a.deductAmount // 减去预留的金额
if a.amount < 0 { // 如果小于0,也就是账号不足,返回错误
return errors.New("insufficient balance")
}
return nil
}
- confirm 函数实现:确认操作,实际业务中可能将冻结金额转出,扣除数据库的之类的
go
func (a *AccountService) Confirm(ctx context.Context) error {
a.frozen = 0
return nil
}
- cancel 函数实现:取消操作,回滚预留的金额
go
func (a *AccountService) Cancel(ctx context.Context) error {
a.amount += a.deductAmount
a.frozen = 0
return nil
}
- 单测:
go
func TestPaymentTCCSuccess(t *testing.T) {
account := NewAccountService("FanOne", 1000.0)
inventory := NewInventoryService("iPhone18", 10)
bizData := struct {// 准备业务数据
OrderID string
Amount float64
Quantity int
}{
OrderID: "FanOne-Apple-Success",
Amount: 500.0,
Quantity: 2,
}
// 设置预留资源
account.PrepareTry(bizData.Amount)
inventory.PrepareTry(bizData.Quantity)
coordinator := NewCoordinator(account, inventory)// 创建TCC协调者
err := coordinator.Execute(context.Background())// 执行TCC事务
assert.NoError(t, err)
// 验证结果
assert.Equal(t, 500.0, account.amount)
assert.Equal(t, 0.0, account.frozen)
assert.Equal(t, 8, inventory.quantity)
assert.Equal(t, 0, inventory.frozen)
}
结果符合预期