TCC 事务¶
可靠消息队列缺少隔离性的问题:
在线书店的场景事例中,如果缺乏了隔离性,就会带来一个显而易见的问题:超售。
在书店的业务场景下,很有可能会出现这样的情况:
两个客户在短时间内都成功购买了同一件商品
而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和,却超过了库存
TCC: Try-Confirm-Cancel:
除可靠消息队列以外的另一种常见的分布式事务机制
它天生适合用于需要强隔离性的分布式事务中。
备注
在具体实现上,TCC 的操作其实有点儿麻烦和复杂,它是一种业务侵入性较强的事务方案,要求业务处理过程必须拆分为 “预留业务资源” 和 “确认 / 释放消费资源” 两个子过程。
TCC 的实现过程分为了三个阶段:
Try
尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好事务需要用到的所有业务资源(保障隔离性)
Confirm
确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。
注意,Confirm 阶段可能会重复执行,因此需要满足幂等性。
Cancel:
取消执行阶段,释放 Try 阶段预留的业务资源。
注意,Cancel 阶段也可能会重复执行,因此也需要满足幂等性。
实例演示TCC 的执行过程:
1. 最终用户向 Fenix's Bookstore 发送交易请求
购买一本价值 100 元的《深入理解 Java 虚拟机》
2. 创建事务,生成事务 ID,记录在活动日志中,进入 Try 阶段
用户服务:检查业务可行性,
可行的话,把该用户的 100 元设置为 “冻结” 状态,通知下一步进入 Confirm 阶段
不可行的话,通知下一步进入 Cancel 阶段
仓库服务:检查业务可行性
可行的话,将该仓库的 1 本《深入理解 Java 虚拟机》设置为 “冻结” 状态,通知下一步进入 Confirm 阶段
不可行的话,通知下一步进入 Cancel 阶段
商家服务:检查业务可行性,不需要冻结资源
3. 如果第二步中所有业务都反馈业务可行,就将活动日志中的状态记录为 Confirm,进入 Confirm 阶段
用户服务:完成业务操作(扣减被冻结的 100 元)
仓库服务:完成业务操作(标记那 1 本冻结的书为出库状态,扣减相应库存)
商家服务:完成业务操作(收款 100 元)
4. 如果第三步的操作全部完成了,事务就会宣告正常结束
异常处理:
1. 如果第三步中的任何一方出现了异常,不论是业务异常还是网络异常
将会根据活动日志中的记录,来重复执行该服务的 Confirm 操作,即进行 “最大努力交付”
2. 如果是在第二步,有任意一方反馈业务不可行,或是任意一方出现了超时
将活动日志的状态记录为 Cancel,进入 Cancel 阶段:
用户服务:取消业务操作(释放被冻结的 100 元)
仓库服务:取消业务操作(释放被冻结的 1 本书)
商家服务:取消业务操作(大哭一场后安慰商家谋生不易)
3. 如果上一步全部完成了,事务就会宣告以失败回滚结束。
而如果第五步中的任何一方出现了异常,不论是业务异常还是网络异常
也都将会根据活动日志中的记录,来重复执行该服务的 Cancel 操作,即进行 “最大努力交付”
备注
TCC 其实有点类似于 2PC 的准备阶段和提交阶段,但 TCC 是位于用户代码层面,而不是在基础设施层面,这就为它的实现带来了较高的灵活性,我们可以根据需要设计资源锁定的粒度。另外,TCC 在业务执行的时候,只操作预留资源,几乎不会涉及到锁和资源的争用,所以它具有很高的性能潜力
备注
由于 TCC 的业务侵入性比较高,需要开发编码配合,在一定程度上增加了不少工作量。所以,通常我们并不会完全靠裸编码来实现 TCC,而是会基于某些分布式事务中间件(如阿里开源的 Seata)来完成,以尽量减轻一些编码工作量。
TCC缺点:
TCC 最主要的限制是它的业务侵入性很强,带来它要求有很强的技术可控性
比如有一个服务是调用支付宝的支付,我们没法让支付宝配合做「资金冻结」之类的操作
这种情况只能考虑采用另外一种柔性事务方案:SAGA 事务