【Distributed System】分布式事务 - TCC

Posted by 西维蜀黍 on 2019-07-12, Last Modified on 2023-02-21

Background

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

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

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

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

进一步思考

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

什么意思呢?也就是说

  1. 订单服务-修改订单状态
  2. 库存服务-扣减库存
  3. 积分服务-增加积分
  4. 仓储服务-创建销售出库单

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

假设现在订单的状态都修改为“已支付”了,结果库存此时库存已经是0,因此该订单下单失败。

结果呢?还是扣了用户的钱,这不是坑人吗?

TCC(Try-Confirm-Cancel)

关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC即为Try Confirm Cancel,它属于补偿型分布式事务(compensating transactions),也是两阶段提交(two-phase commit)的变种。

  1. TCC 的 Try 操作作为一阶段,负责资源的检查和预留;
    • 如果一个participant返回 Try 成功,表明资源的保留已经成功了
  2. Confirm 操作作为二阶段提交操作,对预留资源进行逻辑处理,即标识状态成完成;
  3. Cancel 是二阶段回滚操作,对预留资源进行rollback,使资源回到初始状态。

如下图所示,用户实现 TCC 服务之后,该 TCC 服务将作为分布式事务的其中一个资源,参与到整个分布式事务中;事务管理器分 2 阶段协调 TCC 服务,在第一阶段调用所有 TCC 服务的 Try 方法,在第二阶段执行所有 TCC 服务的 Confirm 或者 Cancel 方法;最终所有 TCC 服务要么全部都是提交的,要么全部都是回滚的。

Demo

在接入 TCC 之后,就需要考虑如何将扣款操作分成 2 步完成:

  • Try 操作:资源的检查和预留;

在扣款场景,Try 操作要做的事情就是先检查 A 账户余额是否足够,再冻结要扣款的 30 元(预留资源);此阶段发生真正的扣款或者记录当前30元处于预留状态

  • Confirm 操作:对预留资源进行逻辑处理,

在扣款场景下,Confirm 阶段会标识扣款状态成完成或将处于预留状态的30元进行扣款;

  • Cancel 操作:预留资源的是否;

在扣款场景下,扣款取消,Cancel 操作执行的任务是释放 Try 操作冻结的 30 元钱,重置预留状态,并将处于预留状态或者已扣款状态的30元回到 A 账户余额中。

容错方式(Failover)

Case 1 - coordinator 宕机,participant 正常

这种情况其实比较好解决,只要找一个coordinator的替代者。当它成为新的coordinator的时候,查看当前本地持久化日志,如果本地日志中没有记录(可能由于 coordinator 还没来得及发出去,或者participant还没有回复,或者回复了但是网络超时了,或者回复了但是还没有达到coordinator,或者到达了coordinator但是还没来得及持久化),则询问 participant,coordinator就能够知道当前全局事务和本地事务处于哪个状态,并继续处理下去。

但是,无论在哪种情况下,新起来的coordinator都会继续处理下去,因而能够保证最终一致性。

Coordinator Fails After sending Try

Coordinator Fails After all Trys succeed

Case 2 - participant 宕机,coordinator正常

这种情况其实也比较好解决。如果coordinator宕机了。那么可能有以下几种情况:

Participant Fails Before sending the response

coordinator会通知所有participant Cancel, 因此不会导致数据不一致的问题。

Participant Fails After sending the response

宕机之后又恢复了,这时取决于宕机的时长:

  • 如果很短,可能没有任何影响(比如宕机后及时恢复的 Participant 仍然收到了 Confirm)
  • 如果很长,可能这时候 coordinator已经发送了Confirm,但并没有被收到,此时这个未被confirm的participant的tcc checker会定时扫描未confirmed的transaction,发现这个local transaction,并询问coordinator的这个global transaction的状态,然后对自己的local transaction进行confirm 或cancel

Feature Analysis

TCC transactions have four features:

  • Atomic: The initiator of the transaction coordinates and ensures that either all the branch transactions are committed or all of them are rolled back.
  • Consistency: TCC transactions ensure eventual consistency.
  • Isolation: TCC implements data isolation by pre-allocating resources in the try phase.
  • Durability: TCC implements durability by coordinating each branch transaction.

The TCC transaction model is intrusive for the business as the business side needs to split one interface to three, which results in high development costs.

TCC drawbacks

  1. Need dev effort
    1. Service need support 3 APIs Vs 1 API
    2. Caller need support callback
  2. Need devops/DBA effor
  3. tA new script
  4. A new table (need archive old transaction record)
  5. Performance
  6. One Transaction, need Try()+Commit() : 1 DB insert + 1 DB updates

Consideration

并发控制

用户在实现 TCC 时,应当考虑并发性问题,将锁的粒度降到最低,以最大限度的提高分布式事务的并发性。

以下还是以A账户扣款为例,“账户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除 30 元,出现并发”。

在一阶段 Try 操作中,分布式事务 T1 和分布式事务 T2 分别冻结资金的那一部分资金,相互之间无干扰;这样在分布式事务的二阶段,无论 T1 是提交还是回滚,都不会对 T2 产生影响,这样 T1 和 T2 在同一笔业务数据上并行执行。

允许空回滚(Allow Null Rollbacks)

如下图所示,事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因为丢包而导致的网络超时,此时事务管理器会触发 Cancel ,调用 TCC 服务的 Cancel 操作,而 Cancel 操作调用未出现超时。

TCC 服务在未收到 Try 请求的情况下收到 Cancel 请求,这种场景被称为空回滚。空回滚在生产环境经常出现,用户在实现TCC服务时,应允许允许空回滚的执行,即收到空回滚时返回成功。

防悬挂控制(Prevent Resource Suspension)

如下图所示,事务协调器在调用 TCC 服务的 Try 操作时,可能会出现因网络拥堵,或者事务管理器收到了另外一个TCC 服务的Try No ACK,因而立刻发送 Cancel 请求l给当前事务管理器(而不是直到收到了这个TCC 服务的Try请求的ACK,才发送Cancel请求)。

因而,出现在Cancel 操作到达之后,对应 Try 数据包才达到的情况,即 Cancel 请求比 Try 请求先执行的情况。如果此 TCC 服务在执行晚到的 Try 时,由于永远不会再收到 Confirm 或者 Cancel ,最终造成 TCC 服务悬挂。

因此,用户在实现 TCC 服务时,要允许空回滚(null rollback),而且要拒绝执行空回滚之后 Try 请求,要避免出现悬挂。

幂等控制(Idempotence)

无论是网络数据包重传,还是异常事务的补偿执行,都会导致 TCC 服务的 Try、Confirm 或者 Cancel 请求被重复调用。

因此,TCC 服务 Owner 在实现 TCC 服务时,需要实现幂等,即每一个 Try、Confirm或Cancel请求被调用一次和执行多次的业务结果是一样的。

Implementation

如果你要玩儿 TCC 分布式事务,必须引入一个 TCC 分布式事务框架,比如国内开源的 ByteTCC、Himly、TCC-transaction

Reference