分布式事务 Seata TCC 模式深度解析
本文档的内容主要分为以下四个部分:
Seata TCC 模式的原理解析;
从 TCC 的业务模型与并发控制分享如何设计一个 TCC 接口,并且适配 TCC 模型;
如何控制异常;
性能优化,使得 TCC 模式能够满足更高的业务需求。
1 Seata 的 TCC 模式
1.1 服务化拆分
下面我们就进入第一个主题,Seata 的 TCC 模式。蚂蚁金服早期是单系统架构,所有业务服务几乎
都在少数几个系统中。随着业务的发展,业务越来越复杂,服务之间的耦合度也越来越高,故我们对
系统进行了重构,服务按照功能进行解耦和垂直拆分。拆分之后所带来的问题就是一个业务活动原来
只需要调用一个服务就能完成,现在需要调用多个服务才能完成,而网络、机器等不可靠,数据一致
性的问题很容易出现,与可扩展性、高可用容灾等要求并肩成为金融 IT 架构支撑业务转型升级的最
大挑战之一。
从图中可以看到,从单系统到微服务转变,其实是一个资源横向扩展的过程,资源的横向扩展是指当
单台机器达到资源性能瓶颈,无法满足业务增长需求时,就需要横向扩展资源,形成集群。通过横向
扩展资源,提升非热点数据的并发性能,这对于大体量的互联网产品来说,是至关重要的。服务的拆
分,也可以认为是资源的横向扩展,只不过方向不同而已。
资源横向扩展可能沿着两个方向发展,包括业务拆分和数据分片:
业务拆分。根据功能对数据进行分组,并将不同的微服务分布在多个不同的数据库上,这实际
上就是 SOA 架构下的服务化。业务拆分就是把业务逻辑从一个单系统拆分到多个微服务中。
数据分片。在微服务内部将数据拆分到多个数据库上,为横向扩展增加一个新的维度。数据分
片就是把一个微服务下的单个 DB 拆分成多个 DB,具备一个 Sharding 的功能。通过这样的
拆解,相当于一种资源的横向扩展,从而使得整个架构可以承载更高的吞吐。
横向扩展的两种方法可以同时进行运用:交易、支付与账务三个不同微服务可以存储在不同的数据库
中。另外,每个微服务内根据其业务量可以再拆分到多个数据库中,各微服务可以相互独立地进行扩
展。
Seata 关注的就是微服务架构下的数据一致性问题,是一整套的分布式事务解决方案。Seata 框架包
含两种模式,一种是 AT 模式。AT 模式主要从数据分片的角度,关注多 DB 访问的数据一致性,当
然也包括多服务下的多 DB 数据访问一致性问题。
另外一个就是 TCC 模式,TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间
调用的一致性问题,保证读资源访问的事务属性。
今天我们主要讲的就是 TCC 模式。在讲 TCC 之前,我们先回顾一下 AT 模式,这样有助于我们理
解后面的 TCC 模式。
1.2 AT 模式
对于 AT 模式,之前其他同学已经分享过很多次,大家也应该比较熟悉了。AT 模式下,把每个数据
库被当做是一个 Resource,Seata 里称为 DataSource Resource。业务通过 JDBC 标准接口访问数
据库资源时,Seata 框架会对所有请求进行拦截,做一些操作。每个本地事务提交时,Seata RM
(Resource Manager,资源管理器) 都会向 TC(Transaction Coordinator,事务协调器) 注册一
个分支事务。当请求链路调用完成后,发起方通知 TC 提交或回滚分布式事务,进入二阶段调用流程。
此时,TC 会根据之前注册的分支事务回调到对应参与者去执行对应资源的第二阶段。TC 是怎么找
到分支事务与资源的对应关系呢?每个资源都有一个全局唯一的资源 ID,并且在初始化时用该 ID
向 TC 注册资源。在运行时,每个分支事务的注册都会带上其资源 ID。这样 TC 就能在二阶段调用
时正确找到对应的资源。
这就是我们的 AT 模式。简单总结一下,就是把每个数据库当做一个 Resource,在本地事务提交时
会去注册一个分支事务。
1.3 TCC 模式
那么对应到 TCC 模式里,也是一样的,Seata 框架把每组 TCC 接口当做一个 Resource,称为 TCC
Resource。这套 TCC 接口可以是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自
动扫描识别到 TCC 接口的调用方和发布方。如果是 RPC 的话,就是 sofa:reference、sofa:service、
dubbo:reference、dubbo:service 等。
扫描到 TCC 接口的调用方和发布方之后。如果是发布方,会在业务启动时向 TC 注册 TCC Resource,
与 DataSource Resource 一样,每个资源也会带有一个资源 ID。
如果是调用方,Seata 框架会给调用方加上切面,与 AT 模式一样,在运行时,该切面会拦截所有对
TCC 接口的调用。每调用一次 Try 接口,切面会先向 TC 注册一个分支事务,然后才去执行原来的
RPC 调用。当请求链路调用完成后,TC 通过分支事务的资源 ID 回调到正确的参与者去执行对应
TCC 资源的 Confirm 或 Cancel 方法。
在讲完了整个框架模型以后,大家可能会问 TCC 三个接口怎么实现。因为框架本身很简单,主要是
扫描 TCC 接口,注册资源,拦截接口调用,注册分支事务,最后回调二阶段接口。最核心的实际上
是 TCC 接口的实现逻辑。下面我将根据蚂蚁金服内部多年的实践来为大家分析怎么实现一个完备的
TCC 接口。
2 TCC 业务模式与并发控制
2.1 TCC 设计原则
从 TCC 模型的框架可以发现,TCC 模型的核心在于 TCC 接口的设计。用户在接入 TCC 时,大部
分工作都集中在如何实现 TCC 服务上。下面我会分享蚂蚁金服内多年的 TCC 应用实践以及在 TCC
设计和实现过程中的注意事项。
设计一套 TCC 接口最重要的是什么?主要有两点,第一点,需要将操作分成两阶段完成。TCC(Try-
Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖 RM 对分布式事务
的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC 模型认为对于业务系统中一个特定的业务逻辑 ,其对外提供服务时,必须接受一些不确定性,
即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果
主业务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操
作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服
务的确认操作。每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC 分
布式事务模型需要业务系统提供三段业务逻辑:
1. 初步操作 Try:完成所有业务检查,预留必须的业务资源。
2. 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务
资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,
保证一笔分布式事务能且只能成功一次。
3. 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
第二点,就是要根据自身的业务模型控制并发,这个对应 ACID 中的隔离性。后面会详细讲到。
2.2 账务系统模型设计
下面我们以金融核心链路里的账务服务来分析一下。首先一个最简化的账务模型就是图中所列,每个
用户或商户有一个账户及其可用余额。然后,分析下账务服务的所有业务逻辑操作,无论是交易、充
值、转账、退款等,都可以认为是对账户的加钱与扣钱。
因此,我们可以把账务系统拆分成两套 TCC 接口,即两个 TCC Resource,一个是加钱 TCC 接口,
一个是扣钱 TCC 接口。
那这两套接口分别需要做什么事情呢?如何将其分成两个阶段完成?下面将会举例说明 TCC 业务
模式的设计过程,并逐渐优化。
我们先来看扣钱的 TCC 资源怎么实现。场景为 A 转账 30 元给 B。账户 A 的余额中有 100 元,
需要扣除其中 30 元。这里的余额就是所谓的业务资源,按照前面提到的原则,在第一阶段需要检查
并预留业务资源,因此,我们在扣钱 TCC 资源的 Try 接口里先检查 A 账户余额是否足够,然后预
留余额里的业务资源,即扣除 30 元。
在 Confirm 接口,由于业务资源已经在 Try 接口里扣除掉了,那么在第二阶段的 Confirm 接口里,
可以什么都不用做。而在 Cancel 接口里,则需要把 Try 接口里扣除掉的 30 元还给账户。这是一
个比较简单的扣钱 TCC 资源的实现,后面会继续优化它。
而在加钱的 TCC 资源里。在第一阶段 Try 接口里不能直接给账户加钱,如果这个时候给账户增加了
可用余额,那么在一阶段执行完后,账户里的钱就可以被使用了。但是一阶段执行完以后,有可能是
要回滚的。因此,真正加钱的动作需要放在 Confirm 接口里。对于加钱这个动作,第一阶段 Try 接
口里不需要预留任何资源,可以设计为空操作。那相应的,Cancel 接口没有资源需要释放,也是一
个空操作。只有真正需要提交时,再在 Confirm 接口里给账户增加可用余额。
这就是一个最简单的扣钱和加钱的 TCC 资源的设计。在扣钱 TCC 资源里,Try 接口预留资源扣除
余额,Confirm 接口空操作,Cancel 接口释放资源,增加余额。在加钱 TCC 资源里,Try 接口无需
预留资源,空操作;Confirm 接口直接增加余额;Cancel 接口无需释放资源,空操作。
2.3 账务系统模型并发控制
之前提到,设计一套 TCC 接口需要有两点,一点是需要拆分业务逻辑成两阶段完成。这个我们已经
介绍了。另外一点是要根据自身的业务模型控制并发。
Seata 框架本身仅提供两阶段原子提交协议,保证分布式事务原子性。事务的隔离需要交给业务逻辑
来实现。隔离的本质就是控制并发,防止并发事务操作相同资源而引起的结果错乱。
举个例子,比如金融行业里管理用户资金,当用户发起交易时,一般会先检查用户资金,如果资金充
足,则扣除相应交易金额,增加卖家资金,完成交易。如果没有事务隔离,用户同时发起两笔交易,
两笔交易的检查都认为资金充足,实际上却只够支付一笔交易,结果两笔交易都支付成功,导致资损。
可以发现,并发控制是业务逻辑执行正确的保证,但是像两阶段锁这样的并发访问控制技术要求一直
持有数据库资源锁直到整个事务执行结束,特别是在分布式事务架构下,要求持有锁到分布式事务第
二阶段执行结束,也就是说,分布式事务会加长资源锁的持有时间,导致并发性能进一步下降。
因此,TCC 模型的隔离性思想就是通过业务的改造,在第一阶段结束之后,从底层数据库资源层面的
加锁过渡为上层业务层面的加锁,从而释放底层数据库锁资源,放宽分布式事务锁协议,将锁的粒度
降到最低,以最大限度提高业务并发性能。
还是以上面的例子举例,“账户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除
30 元,出现并发”。在第一阶段 Try 操作中,需要先利用数据库资源层面的加锁,检查账户可用余
额,如果余额充足,则预留业务资源,扣除本次交易金额,一阶段结束后,虽然数据库层面资源锁被
释放了,但这笔资金被业务隔离,不允许除本事务之外的其它并发事务动用。