腾讯二面:TCC分布式事务 | 图解TCC|用Go语言实现一个TCC

写在前面

前段时间,有同学反馈的一个面试问题,觉得分布式事务有点偏了,但其实也不算偏了,在java领域就有很火很成熟的seata分布式事务框架(阿里和蚂蚁的同学主导,目前在apache孵化)。

之前我们讲过了两阶段提交、三阶段提交(感兴趣可以翻翻历史文章),这篇文章我们就来讲讲TCC分布式事务 两提交阶段的变种,并用go语言简单实现一个TCC。详细代码在 https://github.com/CocaineCong/BiliBili-Code

原理介绍

TCC 分别代表:

  • Try:主要是对业务系统做检测及资源预留
  • Confirm:主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的
  • Cancel:主要是在业务执行错误,需要回滚的状态下执行的业务取消,释放预留资源

Try阶段完成业务的准备工作,Confirm阶段完成业务的提交,Cancel阶段完成事务的回滚。 基本原理如下图所示。

  1. 事务开始时,上层业务应用会向事务协调器注册启动事务。
  2. 业务应用会调用所有服务的Try接口,完成一阶段准备
  3. 接着业务应用调用事务协调器进行提交或者回滚事务。
  4. 事务协调器会根据Try接口返回情况,决定调用Confirm或者Cancel。如果接口调用失败,会进行重试

举个例子

我们就举一个电商平台的业务作为例子,当我们下单的时候,Try阶段需要检查以下:

  1. 检查当前账户的金额是否充足,并预留对应的金额来扣除
  2. 检查当前商品库存是否正常,并预留对应的库存
  3. 检查当前商品和当前账户的订单能否创建成功

当我们Try阶段都执行成功之后,注意我们只是Try,做尝试,而不是真正的执行,就开始执行Confirm阶段:

  1. 减去对应的余额
  2. 减去库存
  3. 创建订单

但如果我们Try阶段没成功,比如库存不足,或者余额不足,就开始执行Cancel阶段,因为我们Try虽然没有真正的执行,但是还是会预留预留一些资源空间出来的,我们需要将这些资源空间回滚。

  1. 回滚预留的余额
  2. 回滚库存
  3. 创建订单

我们可以看到这个TCC分布式事务其实是让应用自己定义数据库操作的粒度,使得降低锁冲突和锁的粒度,提高吞吐量。

当然我们也可以看到TCC方案的不足之处:

  1. 对应用的侵入性强:业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
  2. 实现难度较大且复杂:需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略
  3. 为了满足一致性的要求,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)
}

结果符合预期

相关推荐
要开心吖ZSH14 分钟前
《Spring 中上下文传递的那些事儿》Part 4:分布式链路追踪 —— Sleuth + Zipkin 实践
java·分布式·spring
傻啦嘿哟36 分钟前
Python 办公实战:用 python-docx 自动生成 Word 文档
开发语言·c#
翻滚吧键盘40 分钟前
js代码09
开发语言·javascript·ecmascript
q567315231 小时前
R语言初学者爬虫简单模板
开发语言·爬虫·r语言·iphone
幼稚园的山代王1 小时前
RabbitMQ 4.1.1初体验
分布式·rabbitmq·ruby
百锦再1 小时前
RabbitMQ用法的6种核心模式全面解析
分布式·rabbitmq·路由·消息·通道·交换机·代理
一路向北North1 小时前
RabbitMQ简单消息监听和确认
分布式·rabbitmq·ruby
rzl022 小时前
java web5(黑马)
java·开发语言·前端
时序数据说2 小时前
为什么时序数据库IoTDB选择Java作为开发语言
java·大数据·开发语言·数据库·物联网·时序数据库·iotdb
jingling5552 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架