TCC分布式事务

TCC事务架构图

业务场景介绍

咱们先来看看业务场景,假设你现在有一个电商系统,里面有一个支付订单的场景。

img

那对一个订单支付之后,我们需要做下面的步骤:

  • 更改订单的状态为“已支付”
  • 扣减商品库存
  • 给会员增加积分
  • 创建销售出库单通知仓库发货

这是一系列比较真实的步骤,无论大家有没有做过电商系统,应该都能理解。

进一步思考

好,业务场景有了,现在我们要更进一步,实现一个 TCC 分布式事务的效果。

什么意思呢?也就是说,[1] 订单服务 - 修改订单状态,[2] 库存服务 - 扣减库存,[3] 积分服务 - 增加积分,[4] 仓储服务 - 创建销售出库单。

上述这几个步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。

举个例子,现在订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。

结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑人么,当然不能允许这种情况发生了!

但是如果你不用 TCC 分布式事务方案的话,就用个 Spring Cloud 开发这么一个微服务系统,很有可能会干出这种事儿来。

我们来看看下面的这个图,直观的表达了上述的过程:

img

所以说,我们有必要使用 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。

上面那几个步骤,要么全部成功,如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。

比如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停止执行增加积分和通知出库两个操作。

说了那么多,老规矩,给大家上一张图,大伙儿顺着图来直观的感受一下:

img

落地实现 TCC 分布式事务

那么现在到底要如何来实现一个 TCC 分布式事务,使得各个服务,要么一起成功?要么一起失败呢?

大家稍安勿躁,我们这就来一步一步的分析一下。咱们就以一个 Spring Cloud 开发系统作为背景来解释。

TCC 实现阶段一:Try

首先,订单服务那儿,它的代码大致来说应该是这样子的:

public class OrderService {

    // 库存服务
    @Autowired
    private InventoryService inventoryService;

    // 积分服务
    @Autowired
    private CreditService creditService;

    // 仓储服务
    @Autowired
    private WmsService wmsService;

    // 对这个订单完成支付
    public void pay(){
        //对本地的的订单数据库修改订单状态为"已支付"
        orderDAO.updateStatus(OrderStatus.PAYED);

        //调用库存服务扣减库存
        inventoryService.reduceStock();

        //调用积分服务增加积分
        creditService.addCredit();

        //调用仓储服务通知发货
        wmsService.saleDelivery();
    }
}

如果你之前看过 Spring Cloud 架构原理那篇文章,同时对 Spring Cloud 有一定的了解的话,应该是可以理解上面那段代码的。

其实就是订单服务完成本地数据库操作之后,通过 Spring Cloud 的 Feign 来调用其他的各个服务罢了。

但是光是凭借这段代码,是不足以实现 TCC 分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点儿代码好不好。

首先,上面那个订单服务先把自己的状态修改为:OrderStatus.UPDATING。

这是啥意思呢?也就是说,在 pay() 那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。

这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。

然后呢,库存服务直接提供的那个 reduceStock() 接口里,也别直接扣减库存啊,你可以是冻结掉库存。

举个例子,本来你的库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!

你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。

积分服务的 addCredit() 接口也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。

比如:用户积分原本是 1190,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊!

你可以保持积分为 1190 不变,在一个预增加字段里,比如说 prepare_add_credit 字段,设置一个 10,表示有 10 个积分准备增加。

仓储服务的 saleDelivery() 接口也是同理啊,你可以先创建一个销售出库单,但是这个销售出库单的状态是“UNKNOWN”。

也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢!

上面这套改造接口的过程,其实就是所谓的 TCC 分布式事务中的第一个 T 字母代表的阶段,也就是 Try 阶段。

总结上述过程,如果你要实现一个 TCC 分布式事务,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个 Try 的操作。

这个操作,一般都是锁定某个资源,设置一个预备类的状态,冻结部分数据,等等,大概都是这类操作。

咱们来一起看看下面这张图,结合上面的文字,再来捋一捋整个过程:

img

TCC 实现阶段二:Confirm

然后就分成两种情况了,第一种情况是比较理想的,那就是各个服务执行自己的那个 Try 操作,都执行成功了,Bingo!

这个时候,就需要依靠 TCC 分布式事务框架来推动后续的执行了。这里简单提一句,如果你要玩儿 TCC 分布式事务,必须引入一款 TCC 分布式事务框架,比如国内开源的 ByteTCC、Himly、TCC-transaction。

否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了。

如果你在各个服务里引入了一个 TCC 分布式事务的框架,订单服务里内嵌的那个 TCC 分布式事务框架可以感知到,各个服务的 Try 操作都成功了。

此时,TCC 分布式事务框架会控制进入 TCC 下一个阶段,第一个 C 阶段,也就是 Confirm 阶段。

为了实现这个阶段,你需要在各个服务里再加入一些代码。比如说,订单服务里,你可以加入一个 Confirm 的逻辑,就是正式把订单的状态设置为“已支付”了,大概是类似下面这样子:

public class OrderServiceConfirm {

    public void pay(){
        orderDao.updateStatus(OrderStatus.PAYED);
    }
}

库存服务也是类似的,你可以有一个 InventoryServiceConfirm 类,里面提供一个 reduceStock() 接口的 Confirm 逻辑,这里就是将之前冻结库存字段的 2 个库存扣掉变为 0。

这样的话,可销售库存之前就已经变为 98 了,现在冻结的 2 个库存也没了,那就正式完成了库存的扣减。

积分服务也是类似的,可以在积分服务里提供一个 CreditServiceConfirm 类,里面有一个 addCredit() 接口的 Confirm 逻辑,就是将预增加字段的 10 个积分扣掉,然后加入实际的会员积分字段中,从 1190 变为 1120。

仓储服务也是类似,可以在仓储服务中提供一个 WmsServiceConfirm 类,提供一个 saleDelivery() 接口的 Confirm 逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理人员查看和使用,而不是停留在之前的中间状态“UNKNOWN”了。

好了,上面各种服务的 Confirm 的逻辑都实现好了,一旦订单服务里面的 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执行各个服务的 Confirm 逻辑。

订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进行通信,依次调用各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执行。

同样,给大家来一张图,顺着图一起来看看整个过程:

img

TCC 实现阶段三:Cancel

好,这是比较正常的一种情况,那如果是异常的一种情况呢?

举个例子:在 Try 阶段,比如积分服务吧,它执行出错了,此时会怎么样?

那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。

也就是说,会执行各个服务的第二个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加一些代码。

首先订单服务,它得提供一个 OrderServiceCancel 的类,在里面有一个 pay() 接口的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。

库存服务也是同理,可以提供 reduceStock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存里去,98 + 2 = 100。

积分服务也需要提供 addCredit() 接口的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。

仓储服务也需要提供一个 saleDelivery() 接口的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。

然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。

大家看看下面的图,直观的感受一下:

总结与思考

好了,兄弟们,聊到这儿,基本上大家应该都知道 TCC 分布式事务具体是怎么回事了!

总结一下,你要玩儿 TCC 分布式事务的话:首先需要选择某种 TCC 分布式事务框架,各个服务里就会有这个 TCC 分布式事务框架在运行。

然后你原本的一个接口,要改造为 3 个逻辑,Try-Confirm-Cancel:

  • 先是服务调用链路依次执行 Try 逻辑。
  • 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
  • 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。

这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时:

  • 某个服务的数据库宕机了。
  • 某个服务自己挂了。
  • 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。
  • 某些资源不足了,比如说库存不够这些。

先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。

如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。

接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。

那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。

此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。

等一等,你有没有想到一个问题?如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?

所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。

问题还没完,万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢?

那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功!

如果实在解决不了,这一定是很小概率的事件,这个时候可以让系统发邮件通知人工进行干预处理,因为没有任何一个分布式系统能保证绝对的成功。

当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的!

最后,再给大家来一张图,来看看给我们的业务,加上分布式事务之后的整个执行流程:

不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用,比如我们就是这样。

不过如果自己公司没有研发 TCC 分布式事务框架的话,那一般就会选用开源的框架。

这里笔者给大家推荐几个比较不错的框架,都是咱们国内自己开源出去的:ByteTCC,TCC-transaction,Himly。

大家有兴趣的可以去它们的 GitHub 地址,学习一下如何使用,以及如何跟 Spring Cloud、Dubbo 等服务框架整合使用。

只要把那些框架整合到你的系统里,很容易就可以实现上面那种奇妙的 TCC 分布式事务的效果了。

来源:微信公众号 石杉的架构笔记

开源框架

seata https://seata.apache.org/ go-seata https://github.com/apache/incubator-seata-go

TCC 优缺点

优点

  1. 解决了跨服务的业务操作原子性问题,例如组合支付,订单减库存等场景非常实用。
  2. TCC 的本质原理是把数据库的二阶段提交上升到微服务来实现,从而避免了数据库 2 阶段中锁冲突的长事务低性能风险。
  3. TCC 异步高性能,它采用了 try 先检查,然后异步实现 confirm,真正提交的是在 confirm 方法中。 缺点
  4. 对微服务的侵入性强,微服务的每个事务都必须实现 try,confirm,cancel 等 3 个方法,开发成本高,今后维护改造的成本也高。
  5. 为了达到事务的一致性要求,try,confirm、cancel 接口必须实现等幂性操作。(定时器 + 重试)
  6. 由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个 TCC 事务时间拉长,建议采用 redis 的方式来记录事务日志。
  7. tcc 需要通过锁来确保数据的一致性,会加锁导致性能不高。

因此,TCC 事务在分布式事务中的使用频率并没有那么高。

在 Go 项目中的实现

type OrderService struct{
    CreditSrvclient proto.Creditclient 	//用户积分
    WmsSrvClient proto.WmsClient 	//记录仓库的变动信息
    InventorySrvClient proto.InventoryClient 	//库存确认扣减
}

func NewOrderService()  *OrderService{   
   return &OrderService{
        CreditSrvClient:proto.Creditclient{},
        WmsSrvClient:proto.Wmsclient{},
        InventorySrvClient:proto.InventoryClient{},  
   }    
}

func(o OrderService) UpdateOrderStatus()error {
    return nil
}

func(o Orderservice) Notify() error{
	
    o.Update0rderstatus() //更新订单的状态
    o.CreditSrvclient.Addcredit() //增加积分
    o.InventorySrvclient.Reducestock() //库存确认扣减
    o.WmsClient.SaleDelivery() //记录仓库变更记录
    
    return nil
}

示例

下面代码演示了一个简单的订单系统,其中包含了使用 TCC 事务的部分。OrderService、ProductService 和 AccountService 分别模拟了订单、商品和账户服务。Order、Product 和 Account 分别代表了订单、商品和账户的数据结构。

在 createOrder 函数中,首先执行订单服务的 TryCreateOrder 步骤,检查商品库存是否足够并创建订单。然后执行账户服务的 TryDeductAmount 步骤,扣减账户余额。如果任何一个步骤失败,将执行相应的 Cancel 步骤进行回滚。

如果所有步骤都成功,将执行 Confirm 步骤确认事务,即调用订单服务的 ConfirmCreateOrder 和账户服务的 ConfirmDeductAmount。

在 main 函数中,初始化订单服务、商品服务和账户服务的实例,然后调用 createOrder 函数创建订单。最后输出订单、商品和账户的信息。

package main

import (
	"fmt"
	"log"
	"time"
)

// OrderService 订单服务
type OrderService struct {
	// 模拟订单数据库
	orders map[string]*Order
}

// Order 订单结构体
type Order struct {
	ID        string
	Amount    int
	CreatedAt time.Time
	Status    string
}

// ProductService 商品服务
type ProductService struct {
	// 模拟商品数据库
	products map[string]*Product
}

// Product 商品结构体
type Product struct {
	ID       string
	Name     string
	Price    int
	Quantity int
}

// AccountService 账户服务
type AccountService struct {
	// 模拟账户数据库
	accounts map[string]*Account
}

// Account 账户结构体
type Account struct {
	ID       string
	Balance  int
	Currency string
}

// TryCreateOrder Try 步骤:创建订单
func (os *OrderService) TryCreateOrder(order *Order) error {
	// 检查商品库存是否足够
	product := getProductFromDB(order.ID)
	if product == nil || product.Quantity < 1 {
		return fmt.Errorf("商品库存不足")
	}

	// 扣减商品库存
	product.Quantity--

	// 创建订单
	order.CreatedAt = time.Now()
	order.Status = "Pending"
	os.orders[order.ID] = order

	return nil
}

// ConfirmCreateOrder Confirm 步骤:确认创建订单
func (os *OrderService) ConfirmCreateOrder(order *Order) {
	order.Status = "Confirmed"
	fmt.Println("订单创建成功:", order)
}

// CancelCreateOrder Cancel 步骤:取消创建订单
func (os *OrderService) CancelCreateOrder(order *Order) {
	// 恢复商品库存
	product := getProductFromDB(order.ID)
	if product != nil {
		product.Quantity++
	}

	// 删除订单
	delete(os.orders, order.ID)

	fmt.Println("订单创建取消:", order)
}

// TryDeductAmount Try 步骤:扣减账户余额
func (as *AccountService) TryDeductAmount(accountID string, amount int) error {
	account := getAccountFromDB(accountID)
	if account == nil || account.Balance < amount {
		return fmt.Errorf("账户余额不足")
	}

	// 扣减账户余额
	account.Balance -= amount
	return nil
}

// ConfirmDeductAmount Confirm 步骤:确认扣减账户余额
func (as *AccountService) ConfirmDeductAmount(accountID string, amount int) {
	fmt.Println("账户余额扣减成功:", accountID)
}

// CancelDeductAmount Cancel 步骤:取消扣减账户余额
func (as *AccountService) CancelDeductAmount(accountID string, amount int) {
	account := getAccountFromDB(accountID)
	if account != nil {
		// 恢复账户余额
		account.Balance += amount
	}

	fmt.Println("账户余额扣减取消:", accountID)
}

// getProductFromDB 从数据库获取商品信息
func getProductFromDB(productID string) *Product {
	// 模拟从数据库获取商品信息的操作
	products := map[string]*Product{
		"p1": {ID: "p1", Name: "Product 1", Price: 100, Quantity: 10},
		"p2": {ID: "p2", Name: "Product 2", Price: 200, Quantity: 5},
	}

	return products[productID]
}

// getAccountFromDB 从数据库获取账户信息
func getAccountFromDB(accountID string) *Account {
	// 模拟从数据库获取账户信息的操作
	accounts := map[string]*Account{
		"a1": {ID: "a1", Balance: 500, Currency: "USD"},
		"a2": {ID: "a2", Balance: 1000, Currency: "USD"},
	}

	return accounts[accountID]
}

// createOrder 创建订单
func createOrder(order *Order, productID string, accountID string, orderService *OrderService, productService *ProductService, accountService *AccountService) error {
	// 执行 Try 步骤
	err := orderService.TryCreateOrder(order)
	if err != nil {
		return err
	}

	err = accountService.TryDeductAmount(accountID, order.Amount)

	if err != nil {
		// 回滚订单创建步骤
		orderService.CancelCreateOrder(order)
		return err
	}

	// 执行 Confirm 步骤
	orderService.ConfirmCreateOrder(order)
	accountService.ConfirmDeductAmount(accountID, order.Amount)

	return nil
}

func main() {
	// 初始化服务
	orderService := &OrderService{orders: make(map[string]*Order)}
	productService := &ProductService{products: make(map[string]*Product)}
	accountService := &AccountService{accounts: make(map[string]*Account)}

	// 创建订单
	order := &Order{
		ID:     "order1",
		Amount: 100,
	}
	productID := "p1"
	accountID := "a1"

	err := createOrder(order, productID, accountID, orderService, productService, accountService)
	if err != nil {
		log.Println("创建订单失败:", err)
	}

	// 输出订单和商品信息
	fmt.Println("订单信息:", order)
	fmt.Println("商品信息:", productService.products[productID])
	fmt.Println("账户信息:", accountService.accounts[accountID])
}

请注意,这只是一个简单的示例,真正的订单系统可能需要更复杂的业务逻辑和数据管理。此示例仅用于演示如何使用 TCC 事务处理订单创建过程,并且没有考虑并发和事务状态管理等问题。实际使用时,需要根据具体需求进行适当的扩展和优化。

使用 Hugo 构建
主题 StackJimmy 设计