Skip to content

Latest commit

 

History

History
239 lines (118 loc) · 8.86 KB

README_tcc.md

File metadata and controls

239 lines (118 loc) · 8.86 KB

TCC

理论

论文

Pat Helland于2007年发表tcc论文《Life beyond Distributed Transactions: an Apostate’s Opinion》,提出了TCC的概念,在论文中,TCC还是以Tentative-Confirmation-Cancellation命名的,后来Atomikos公司改名为Try-Confirm-Cancel。

TCC事务相对于传统事务(XA, Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的接口调用来实现分布式事务。

理论

TCC事务有一系列子事务构成,每个子事务所属的RM需要提供Try-Confirm-Cancel三个接口来给事务协调者调用。

TCC和2PC类似,也是分为两个阶段,Try阶段和Confirm或Cancel阶段。

  • 第一阶段Try:进行资源检查与预留。尝试执行,完成所有业务一致性检查,预留业务资源,但不会对资源进行锁定。
  • 第二阶段
    • Confirm:确认,真正执行业务,处理Try 阶段预留的业务资源,Confirm 操作要求具备幂等设计,只要进入到Confirm阶段,则事务就已经处于提交状态了,所以Confirm 失败后需要进行重试直到成功为止。
    • Cancel:取消,释放 Try 阶段预留的业务资源。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致,要求满足幂等设计。

TCC不会对在数据资源层面对资源进行锁定,但是会进行业务层面的预留,以将资源层的加锁升级到业务层上,这样业务层可以灵活实现隔离性,达到准隔离性的同时提高并发性能。

举例

仍然使用转账的例子:A和B账户余额都是100元,A转账30元给B。
这个TCC事务有两个子事务,

  • 子事务A:从A的账户减去30元,提交后A的账户余额为70元
  • 子事务B:给B的账户添加30元,提交后B的账户余额为130元

需要在资源层将增加一个资源字段冻结余额

执行事务:

  • 第一阶段,Try:
    • 检查A的余额是否大于等于30元,利用资源层事务的原子性,在A的冻结余额中增加-30元,不修改A的余额,所以A的账户余额还是100元。其他事务看到的余额总数还是100元。
    • 将B的冻结余额增加+30元,不修改B的账户余额,其他事务看到的B的账户余额仍然是100元。
  • 第二阶段,
    • Confirm:
      • 将A的账户的冻结余额的-30元加到账户余额中,那么A的账户余额等于70元。
      • 将B的账户的冻结余额的+30元加到账户余额中,B的账户余额等于130元。
    • Cancel:
      • 将A的账户的冻结余额清空
      • 将B的账户的冻结余额清空

隔离性:

事务执行期间,A和B的账户余额还是100元,但是如果其他事务需要并发修改A或B的账户余额,则需要考虑冻结余额

  • 如果其他事务需要修改A的账户余额,则需要查看冻结余额,发现已经冻结了-30元,则只有70元是可用的
  • 如果其他事务需要修改B的账户余额,则需要查看冻结余额,发现已经冻结了+30元,则只有100元是可用的

或者事务执行期间,不允许其他事务修改A和B的的账户余额

所以,将锁从资源层上升到业务层,隔离性更加灵活,需要如何实现隔离性,完全由业务决定,也避免了资源层并发带来的回滚。

使用场景

  • 适用于业务流程短的事务
  • 隔离性较好:对中间状态有约束的业务

优点

  • 并发度较高,不需要像XA事务对资源进行锁定
  • 隔离性比Saga好,取决于业务实现

缺点

  • 对业务有侵入性:需要实现Try,Confirm,Cancel接口
  • 参与者需要实现幂等

设计

基础概念

TCC事务由三种角色组成

  • TCC事务提交者:事务发起者,在分布式事务中统称为AP
  • TCC事务协调者:接受AP的事务请求,管理TCC事务,在分布式事务中统称为事务协调者(TC : Transaction Coordinator)
  • TCC子事务参与者:协调者将子事务提交给参与者执行, 在分布式事务中统称为资源管理器(RM:Resource Manager)。参与者需要包证接口的幂等性

事务状态

  • committed : 已经提交。最终态
  • aborted : 已经回滚。最终态
  • failed : 回滚中或提交失败(可能重试中)。非终态
  • prepared:提交中。非终态

TCC事务协调者的实现

TCC Service

  • 提供RESTful API,AP通过API进行事务提交或查询
  • TCC service将请求封装成TCC事务对象,提交给TCC Executor执行
  • API结果响应:
    • 如果是同步请求,则等待TCC Executor处理完成,再将结果封装成TccResponse给AP
    • 如果是异步请求,则马上返回给AP
  • 通知:TC通知 AP TCC事务执行的结果

TCC Executor

  • TCC事务真正的执行者,管理TCC事务的状态转换,重试,持久化等等,不涉及与RM和AP的交互
  • TCC子事务处理和回滚:TCC Executor并不处理子事务,而是将子事务操作写入Channel,由外层去向RM发送子事务请求
  • TCC事务通知:TCC Executor不执行具体的通知操作,而是将操作写入Channel,由外层去执行AP
  • 数据持久化:TCC Executor将TCC数据存储到本地数据库中来保证数据的持久化,也同时依赖本地数据库的事务特性。

事务流程

  1. 开启TCC事务:AP调用 TC的prepare接口,开启一个TCC事务

  2. 循环处理所有子事务:如果某个步骤失败,重试策略取决于AP

    1. 注册子事务(分支事务):AP调用TC register接口,注册一个子事务,注册成功再执行下一步
    2. Try:AP调用RM的Try接口
  3. Confirm:如果所有子事务都成功注册和执行try,则AP调用TC的Confirm接口,告知事务可以执行提交

    1. TC循环调用所有参与子事务的RM的Confirm接口,如果返回失败则会一直重试指导成功
  4. Cancel:如果子事务执行失败,AP不进行重试则调用TC的Cancel接口取消TCC事务;或者达到TCC事务过期时间,TC会自行取消TCC事务。

    1. 循环调用所有子事务的RM,调用RM的Cancel接口

异常情况处理

分布式事务实现的一个难点就是时序问题,主要体现在:

  • 服务器的时钟不同步
  • 请求乱序

因此会产生一些不可预测的异常。

TCC事务过期

如果TCC事务过期,则TC需要先将事务的状态标记为"需要Cancel"状态,再调用RM的Cancel接口来取消子事务。

如果后续TC接受到此TCC事务的Confirm请求,TC应先查看本地数据库中此事务状态,如果已经处于Cancel或者已经Cancel,则应该拒绝Confirm请求。

同时,TC对数据库中事务的状态修改,应该使用数据库事务来确保隔离性和一致性,因为可能有多个TC同时存在。

回滚异常

异常流程如下:

  • TC向RM发送Try请求
  • 由于网络原因Try请求仍然处于发送中,没有到达RM
  • AP调用TC取消TCC事务,或者TCC事务过期,TC自行取消
  • TC调用RM的Cancel接口取消子事务
  • 异常点1 :RM收到Cancel请求,发现此子事务没有执行过Try,产生异常
  • 异常点2 :当RM收到Cancel请求后,之前由于网络原因阻塞的Try请求到达RM,如果RM执行这个Try,则会产生数据不一致的异常。
  • 异常点3:由于重试策略,导致AP向RM发送了多于1次的Try请求,或者TC向RM发送了多次Confirm或者Cancel请求。

所以,为了避免上面异常情况,需要进行如下检查

  • RM收到Try操作, 检查是否有Try记录
    • 如果有记录:如果已经执行过Try,直接返回成功,确保幂等性
    • 如果没有Try记录,则检查是否有Cancel记录,
      • 如果有Cancel记录,则拒绝执行Try
      • 如果没有Cancel记录,则执行Try,且更改子事务状态为"已经执行try"。
  • RM收到Cancel操作,查看此子事务是否有Cancel记录,
    • 如果有Cancel记录,则直接返回Cancel成功,确保幂等性
    • 如果没有Cancel记录,则检查是否有Try记录
      • 如果有Try记录,则执行Cancel动作
      • 如果没有Try记录,则将此Cancel请求记录下来,如果后续收到Try请求,则应该拒绝Try请求

本地事务

无论是RM还是TC或AP,在修改其数据时要考虑时序问题和时钟漂移问题导致的乱序,利用本地数据库的事务隔离(可序列化级别)特性来检查事务状态和修改状态。

如:

RM收到Try请求后,需要检查是否有Cancel记录或Try记录,没有才能需要执行Try。检查和执行需要在一个事务中,避免其他线程或进程同时对子事务进行Cancel或者Try修改。