TCC简介
TCC(Try-Confirm-Cancel)是一种基于业务补偿的分布式事务解决方案 ,核心思想通过三个阶段保证分布式事务的一致性:
1. Try(尝试)
各参与服务预执行业务检查(资源预留),如库存冻结、账户余额预扣等
2. Confirm(确认)
所有Try成功时,协调者触发Confirm操作,各服务正式提交事务(如实际扣减库存、划转金额)。
3. Cancel(取消)
任一Try失败时,协调者触发Cancel操作,各服务回滚Try阶段的预留资源(如解冻库存、返还预扣金额)。
为什么可普通交易可以用TCC
因为作为普通交易(normalBuy),主要处理的是非秒杀流量,那么就意味着没有热点问题,那么就是下单的时候同步处理库存扣减和创建订单即可,然后再异步更新redis的库存。
相比于秒杀的方案,秒杀方案不管是哪种,其实都是数据库的扣减和订单创建都是在异步链路上的,通过重试来保证最终一致性的。而普通交易没有这个重试机制,那么我们就引入TCC来保障一致性。
普通交易的TCC如何拆解
那么,映射到我们的交易下单流程中,主要涉及到商品和订单两个模块,那么分别作TCC的话,可以按照以下方式进行拆解:
Try:
- 订单:预创建订单,订单状态未CREATE,用户看不到,也无法支付
- 商品(库存):冻结库存,被冻结的库存无法下单,只能解冻
Confirm:
- 订单:确认订单,从CREATE推进到CONFIRM,这时候订单就可以支付了,用户也能看到
- 商品(库存):解冻并扣减库存(原子操作)
Cancel(Try后的Cancel)
- 订单:废弃订单,从CREATE推进到DISCARD,用户看不到,也做任何操作
- 商品(库存):解冻库存,被解冻的库存就可以下单了。
Cancel(Confirm后的Cancel)
- 订单:废弃订单,从CONFIRM推进到DISCARD,用户看不到,也做任何操作
- 商品(库存):回退被扣减的库存。
具体流程有以下5种情况:
1、Try失败,Cancel成功
2、Try失败,Cancel失败
3、Try成功,Confirm成功
4、Try成功,Confirm失败,Cancel成功
5、Try成功,Confirm失败,Cancel失败
这里面的2种Cancel失败的情况,一般只会在系统异常的时候才会发生,比如数据库挂了,应用挂了等等,所以我们可以考虑通过MQ的方式来进行Cancel,这样如果失败了,MQ可以不断重试。
而且Cancel的操作放在异步链路上,就可以更好的保障同步下单链路的稳定性,异步回退稍微晚一点其实影响不大。
所以,我们重点看上面的1、3、4三种情况。
为了解决空回滚和悬挂的问题,我们引入了事务日志的机制,通过 MySQL 的本地事务来保证业务操作和事务日志的写入的原子性。
详细说明请参考:
TCC的几种情况
Try-Cancel:
第一种最简单,就是Try失败了,然后执行Cancel:
这个情况下,我们为了不让Cancel在同步链路上占用接口耗时,并且Cancel的回退晚一点也没关系,所以我们通过MQ来做Cancel的异步处理,并且利用MQ的重试机制来保证Cancel的最终处理成功。
Try-Confirm:
这个链路其实是系统中流量最大的链路,大部分场景都是走的这个分支,即Try和Confirm都能成功。
这个过程全部同步,代码执行完之后,将全部处于Confirm状态,数据是一致的。
Try-Confirm-Cancel:
这个是另外一个情况, 就是Try成功了,但是Confirm的时候有人失败了。
这个就稍微复杂一点了。前面的try成功,之后confirm失败了,但是为了提升Confirm成功率,避免因为网络延迟、网络抖动带来的大量回滚,我们会尽最大努力的进行confirm,因为try都成功了,confirm成功的概率是非常大的。所以我们引入了重试的机制。
但是如果3次重试之后还是失败,那就要启动cancel的流程了,同样我们把cancel也放到了MQ的异步链路上。
但是为了避免在cancal消息接到的时候,confirm又成功了,所以cancel这里会先发一个疑似废单消息,在这个消息里再检查一下订单状态,如果成功了,则不需要执行了。如果没成功,则再走Try-Cancel的废单流程。
这里面需要注意的是,对于订单、商品(藏品、盲盒)系统模块来说,他们是不关心这个疑似废单消息的,这个消息交易模块自己包掉。他们只关心真正的废单消息的处理。