TCC概念
常见的分布式事务实现方案有以下几种:两阶段提交(2PC)、三阶段提交(3PC)、TCC(Try-Confirm-Cancel)、补偿事务(Saga)、MQ事务消息等。
由于2PC资源锁定时间较长,性能较差,难以扩展。TCC旨在解决这些问题,TCC是一种补偿型事务模式,通过将每个事务操作分为三个阶段:尝试(Try)、确认(Confirm)和取消(Cancel),不仅优化了事务的执行流程,还提高了系统的整体效率和弹性。
TCC流程
TCC的核心思想在于将事务的每个操作拆分为三个明确的阶段:尝试(Try)、确认(Confirm)和取消(Cancel)。这三个阶段各自承担不同的职责,确保事务在分布式环境中能够安全且一致地执行。
- Try 阶段
在这个阶段,事务参与者将执行所有必要的检查,并预留必需的资源来保证事务可以顺利完成。例如,在电商场景,当用户尝试购买商品时,系统将在Try阶段检查商品库存,预留所需数量的商品。这个阶段是整个TCC流程中的准备步骤,它不会做任何实际的业务处理,只是确保后续的Confirm阶段可以无障碍地执行。
- Confirm 阶段
只有当所有参与者的Try阶段成功完成后,才会进入Confirm阶段。在这个阶段,系统将正式执行业务操作,使用在Try阶段预留的资源。继续举例电商场景,如果用户的支付成功,系统将在Confirm阶段正式从库存中扣除商品,完成交易。这一步骤确保了事务的最终一致性和数据的正确性。
- Cancel 阶段
如果在Try阶段或Confirm阶段中的任何一个环节发生错误,或者某些业务条件未得到满足,整个事务将进入Cancel阶段。在这个阶段,所有已经预留的资源将被释放,所有在Try阶段所做的准备工作将被撤销。例如,如果用户决定取消购买,或者支付未成功,预留的商品库存将被释放,以便其他用户购买。
电商场景介绍
举一个常见的电商场景,用户购买商品,点击支付后。这时候交易系统需要做的操作是:
- 更新订单状态为"已支付"
- 扣减库存
- 增加用户积分
电商系统一般采用微服务架构,上述三个操作分别需要调用三个服务完成。流程如下:
- 订单服务,更新订单状态为"已支付"
- 库存服务,扣减库存
- 积分服务,增加用户积分
如果这三个操作在一个服务中,就可以使用本地事务保证订单、库存、积分数据的一致性,但是现在需要调用三个服务,就无法保证数据的一致性了。 如果更新订单状态为"已支付"成功了,但是扣减库存失败了,其他用户就可以继续购买商品,可能会出现超卖问题,这时候就需要引入分布式事务,用来保证分布式系统中数据的一致性。
TCC落地电商场景
把上述电商场景引入TCC事务之后,就变成以下情况。
- Try阶段实现
Try阶段负责资源的预检查和预留,分别调用三个服务冻结资源:
- 订单服务,更新订单状态为"支付中"
- 库存服务,冻结库存(库存表中,新增一个冻结库存的字段)
- 积分服务,预增加用户积分(积分表,新增一个预增加积分的字段)
库存表中,新增了一个冻结库存的字段。在商品详情页面展示剩余库存数量的时候,需要减去冻结库存,防止超卖。
- Confirm 阶段
当Try阶段都执行成功的时候,就进入Confirm阶段,负责确认资源。流程如下:
- 订单服务,更新订单状态为"已支付"
- 库存服务,把冻结库存扣减到剩余库存上(update 剩余库存=剩余库存-冻结库存,冻结库存=0)
- 积分服务,把预增加积分加到总积分上(udpate 总积分=总积分+预增加积分,预增加积分=0)
- Cancel阶段
当Try阶段任何一个操作失败,就进入Cancel阶段,负责回滚资源。流程如下:
- 订单服务,更新订单状态为"已取消"
- 库存服务,释放冻结的库存
- 积分服务,释放预增加积分
TCC问题
上述只是理想情况,在实践的过程中会遇到以下三大问题,影响TCC事务的安全性。
- Confirm/Cancel操作失败问题
- 空回滚问题
- 悬挂问题
针对每个问题,我们思考一下相应的解决方案。
1. Confirm/Cancel操作失败问题
在执行完Try阶段之后,进入到Confirm或者Cancel阶段,可能由于服务问题或者网络问题,Confirm或者Cancel操作不一定能成功,通常采用的办法是不断重试,要求Confirm和Cancel接口要支持幂等。
2. 空回滚问题
空回滚问题是指Try阶段某个服务的Try操作没有执行成功或者没有执行,进入Cancel阶段,就执行Cancel操作回滚资源,导致数据错乱。 常用的解决方案是Cancel操作前增加事务状态检查。
- 在 Try 操作开始时,设置事务状态为 TRYING。
- 如果 Try 操作成功,更新状态为 TRIED。
- 在 Cancel 操作执行前,检查状态。如果状态不是 TRIED,就不执行 Cancel 操作。
3. 悬挂问题
悬挂问题是指 Cancel 操作比 Try 操作先完成。通常出现的原因是由于网络延迟,例如:由于调用Try操作耗时较长,出现网络超时,导致Try操作调用失败,就进入Cancel阶段,执行Cancel操作,而此时Try操作还在执行,最后结果是Cancel操作执行成功后,Try操作又执行成功,出现"悬挂"问题。
解决方案有以下几种:
- 与空回滚解决方案类似,Cancel操作前增加事务状态检查。
- 在 Try 操作开始时,设置事务状态为 TRYING。
- 如果 Try 操作成功,更新状态为 TRIED。
- 在 Cancel 操作执行前,检查状态。如果状态不是 TRIED,就不执行 Cancel 操作或者延迟执行。
- Try操作引入重试机制
如果调用Try操作超时,可以进行有限次重试,而不是立即执行Cancel操作,可以减少因为网络超时而导致的悬挂问题。这里要求Try接口也要支持幂等操作。
- 增加同步机制
可以使用分布式锁来控制Try和Cancel操作的执行顺序。
总结
TCC模型将事务分为Try、Confirm和Cancel三个阶段,使得事务处理更加灵活和可控,保证了数据的一致性,减少了2PC资源锁定时间过长的问题。但是也引入了一些新问题:
- 代码侵入严重。Try、Confirm和Cancel三个阶段的事务操作都要耦合在业务逻辑中,耦合性较高。
- 设计复杂,开发成本较高。对于TCC引入的幂等操作、空回滚问题、悬挂问题,都需要架构设计的时候考虑相应的解决方案。
需要开发人员自行评估使用成本。