TCC - 一个多账户扣费的案例

9/14/2022

# 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.正常流程

  1. 用户 A 给主播 B送礼,礼物价值 = 100银币 = 100金币
  2. 添加一条业务记录,生成自增id 123;业务状态是未扣费;
  3. 调用银币预扣接口,银币预扣需要传唯一标识以保证幂等性,唯一标识 123:silver;此外还有扣费金额 100
  4. 调用银币接口返回30,表明扣了30银币,还需要扣除70金币
  5. 调用金币预扣接口,幂等唯一标识: 123:gold;金币预扣70
  6. 金币预扣成功则提交银币,并提交金币
  7. 修改业务状态为已扣除

# 2.主流程中断

当中间出现了问题,主流程怎么处理

汇总流程如下图。

所有非判断流程(非菱形)失败后,都直接报系统忙,后续工作由补偿脚本处理。

接下来对流程判断(菱形部分)的分支做单独注解。

# 2.1预扣银币不足或失败

银币不足并不算失败,因为还可以继续扣金币;

如果是扣费失败或超时,那直接算失败,因为后续流程中断了。

余额不足可以直接告知,其他错误报系统忙;后续工作由补偿脚本处理。

# 2.2预扣金币不足或失败

预扣金币和银币不同,预扣金币余额不足会导致后续流程中断。

  • 如果金币不足,则回滚银币扣除;报余额不足;
  • 如果扣金币失败,也回滚银币扣除;报余额不足;
  • 如果扣金币超时,不能当失败处理,应该直接结束流程;报系统忙;后续工作由补偿脚本处理。

# 2.3 提交子事务失败

业务走到提交的阶段,后续操作必须成功。

因此按理说如果提交金币或者银币失败,都应该直接结束流程,报系统忙,后续工作由补偿脚本处理。

# 2.4修改业务状态失败

直接结束流程,报系统忙,后续工作由补偿脚本处理。

# 3.补偿脚本逻辑

补偿脚本要根据各种情况做善后处理。

  • 首先查询业务表中业务状态为未扣费,且创建时间超过一定时间的记录。这样可以避开正在进行中的记录。
  • 根据唯一标识查询银币系统的扣除状态;
    • 如果银币是扣除但未提交,则回滚所有事务,修改业务状态为:操作失败;
    • 如果银币是已提交,则提交所有事务,修改业务状态为:已扣除;并进行后续逻辑;
  • 如果银币扣费不存在记录,说明直接扣的金币;进而判断金币扣除状态
    • 如果金币已扣除但未提交,则回滚金币扣除,修改业务状态为:操作失败
    • 如果金币是已提交,则提交所有事务,修改业务状态为:已扣除;并进行后续逻辑;

可以看到补偿脚本的逻辑中,是完全以第一个子事务的状态为判断依据的。

  • 银币已提交则全量提交;银币未提交,则全量回滚
  • 如果银币扣费记录不存在,则金币扣费成为了第一个子事务,其成功与否决定了后续流程是否进行。

# E.小结

TCC在分布式事务中的应用远远不如幂等设计和本地事务。

因为大多数场景仅依赖重试即可解决。

TCC仅适用于有回滚需求的场景。