补偿事务(TCC)
Last updated
Last updated
TCC(Try-Confirm-Cancel)又被称补偿事务,是一种应用层面侵入业务的两阶段提交,是一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认(Try)和补偿(Cancel)操作。
第一阶段 Try(尝试):主要是对业务系统做检测及资源预留(加锁,锁住资源)
第二阶段 本阶段根据第一阶段的结果,决定是执行 Confirm 还是 Cancel
以电商下单为例,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
完成所有业务检查(一致性)。
预留必须业务资源(准隔离性)。
Try 尝试执行业务。
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。 Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作。
这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段。
Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。
TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。也就是说只要 Try 成功,Confirm 一定成功 。
Confirm 与 Cancel 如果失败,由 TCC 框架进行重试。
存在极低概率在 Confirm 与 Cancel 环节彻底失败,则需要定时任务或人工介入。
出现原因:分支事务所在系统服务宕机或者网络波动,分支事务的调用是失败的,该情况下其实分支事务没有进行 Try 操作,当故障恢复后,分布式事务进行了回滚则会调用二阶段的 Cancel 方法,既而形成了空回滚。
解决方法:已知全局事务 id 会贯穿整个全局分布式事务的调用链,额外增加一张分支事务记录表,其中有全局事务 id 和分支事务 id,每一次成功的 Try 执行后插入一条分支事务执行记录,第二阶段 Cancel 执行时读取该表记录,如果该分支事务对应的执行记录存在,就回滚,如果不存在就认为是空回滚,直接返回成功。
出现原因:由于服务宕机或者网络问题,方法的调用可能出现超时,为了保证事务正常执行往往会加入重试的机制,因此必须保证 Confirm 和 Cancel 阶段操作的幂等性。
解决方法:可以在分支事务记录表中增加事务执行状态,每次执行 Confirm 和 Cancel 方法时都查询该事务的执行状态,以此判断事务的幂等性。
出现原因:TCC 中,在调用 Try 之前会先注册分支事务,注册分支事务之后,调用出现超时,此时 Try 请求还未到达对应的服务,因为调用超时了,所以会执行 Cancel 调用。当 Cancel 执行完后,之前发送的 Try 请求却到达了,而这个迟到的 Try 操作不会有后续的 Confirm 或 Cancel,这样会导致资源挂起,无法释放。
解决方法:执行 Try 方法时借助分支事务表中事务的执行状态,可以判断 Confirm 或者 Cancel 方法是否已经执行,如果执行了那么就不执行 Try 阶段。
在 TCC 模式下,所有操作要保证幂等性
需要有事务的执行记录
执行 Try 和 Cancel 时候都需要进行操作记录判断
优势:TCC 执行的每一阶段都会提交本地事务并释放锁,并不需要等待其他事务的执行结果。而如果其他事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
缺点:
代码侵入:需要编写代码实现 Try 、Confirm、 Cancel
开发成本高:一个业务需要拆分成 3 个步骤,分别编写业务实现,业务编写比较复杂
安全性考虑:Cancel 动作如果执行失败,资源就无法释放,需要引入重试机制,而重试导致重复执行,需要考虑重试的幂等性问题