TCC - 一个多账户扣费的案例
# A.理解TCC
之前的案例都是不支持事务回滚的,也就是必须成功。
但是特殊情况下,我们必须要用到回滚的情况,这时候需要TCC的方案。
TCC全称 Try-Confirm-Cancel:
- 先准备资源 - try
- 如果都准备成功则提交事务 - confirm
- 如果有一个准备不成功则取消事务 - cancel
# B.场景
直播间有一种礼物X,用户给主播送该礼物时,可以先消费银币,余下的由金币补足。
金币和银币分属不同的系统。
我们把送礼当做一个分布式事务,这个事务里包含着两个子事务:扣银币和扣金币。
# C.方案
这类属于有回滚需求的分布事务。
假设用户给主播送B礼物,消费完银币,发现金币不足以支付剩下的余额,那么事务就需要回滚。
如果赠送给主播的银币,立刻就被主播消费掉了,那么就无法回滚了。
因此为了保证能回滚,银币不能立刻到账。
TCC事务中,try 阶段锁定的资源必须处理能回滚的状态。
因此这里消费银币可以理解为冻结操作,而非真正转移扣除。就是为了避免另一方一旦到账,会有不可回滚的风险。
# 1.confirm
当银币扣完,金币也扣完后,需要做第二步confirm。
然而confirm也是分步骤提交:如果银币confirm成功,但金币confirm失败呢?
TCC从完成第一个子事务的confirm开始,就变成不能回滚了,后续步骤一定要成功。
其实,资源都已经锁定了,还有什么理由不成功的呢?如果是因为故障导致暂时不能成功,那就不断重试,直到最后成功 。
因此,如果出现银币confirm成功,但金币confirm失败,那就要持续进行金币的confirm,直至成功。
这里需依赖善后脚本,自行实现不断重试。
# 2.cancel
如果银币扣除成功,但是金币扣除失败,导致事务回滚。
此时第一个子事务(扣银币)还是try状态,是可以回滚的。
但是要求金币扣除必须是明确的失败,才能回滚。
如果是超时,那是不能按失败来回滚的。因为超时不意味着失败。
回滚也是必须成功的,try阶段的操作就是为了保证资源处于一个既能提交,又能回滚的状态上。
# D.落地
# 1.正常流程
- 用户 A 给主播 B送礼,礼物价值 = 100银币 = 100金币
- 添加一条业务记录,生成自增id 123;业务状态是未扣费;
- 调用银币预扣接口,银币预扣需要传唯一标识以保证幂等性,唯一标识 123:silver;此外还有扣费金额 100
- 调用银币接口返回30,表明扣了30银币,还需要扣除70金币
- 调用金币预扣接口,幂等唯一标识: 123:gold;金币预扣70
- 金币预扣成功则提交银币,并提交金币
- 修改业务状态为已扣除
# 2.主流程中断
当中间出现了问题,主流程怎么处理
汇总流程如下图。
所有非判断流程(非菱形)失败后,都直接报系统忙,后续工作由补偿脚本处理。
接下来对流程判断(菱形部分)的分支做单独注解。
# 2.1预扣银币不足或失败
银币不足并不算失败,因为还可以继续扣金币;
如果是扣费失败或超时,那直接算失败,因为后续流程中断了。
余额不足可以直接告知,其他错误报系统忙;后续工作由补偿脚本处理。
# 2.2预扣金币不足或失败
预扣金币和银币不同,预扣金币余额不足会导致后续流程中断。
- 如果金币不足,则回滚银币扣除;报余额不足;
- 如果扣金币失败,也回滚银币扣除;报余额不足;
- 如果扣金币超时,不能当失败处理,应该直接结束流程;报系统忙;后续工作由补偿脚本处理。
# 2.3 提交子事务失败
业务走到提交的阶段,后续操作必须成功。
因此按理说如果提交金币或者银币失败,都应该直接结束流程,报系统忙,后续工作由补偿脚本处理。
# 2.4修改业务状态失败
直接结束流程,报系统忙,后续工作由补偿脚本处理。
# 3.补偿脚本逻辑
补偿脚本要根据各种情况做善后处理。
- 首先查询业务表中业务状态为未扣费,且创建时间超过一定时间的记录。这样可以避开正在进行中的记录。
- 根据唯一标识查询银币系统的扣除状态;
- 如果银币是扣除但未提交,则回滚所有事务,修改业务状态为:操作失败;
- 如果银币是已提交,则提交所有事务,修改业务状态为:已扣除;并进行后续逻辑;
- 如果银币扣费不存在记录,说明直接扣的金币;进而判断金币扣除状态
- 如果金币已扣除但未提交,则回滚金币扣除,修改业务状态为:操作失败
- 如果金币是已提交,则提交所有事务,修改业务状态为:已扣除;并进行后续逻辑;
可以看到补偿脚本的逻辑中,是完全以第一个子事务的状态为判断依据的。
- 银币已提交则全量提交;银币未提交,则全量回滚
- 如果银币扣费记录不存在,则金币扣费成为了第一个子事务,其成功与否决定了后续流程是否进行。
# E.小结
TCC在分布式事务中的应用远远不如幂等设计和本地事务。
因为大多数场景仅依赖重试即可解决。
TCC仅适用于有回滚需求的场景。