分布式事务Tcc的具体实现和常见问题

1、场景假设

为了模拟分布式事务,我们现在假定有3个微服务,分别是:

  • 订单
  • 库存
  • 会员

调用的业务逻辑如图下所示:

tcc业务逻辑图

这里涉及到的一个分布式事务问题是:当扣余额失败之后,库存必须恢复到原来的数量而且创建的订单必须撤销。

2、Tcc分布式事务原理

tcc也叫二阶段提交,第一阶段是try。第二阶段是confirm或者cancel

在第一阶段try,让所有人确认:”资源足够吗?并预留资源”。

如果所有的参与者都回答没有问题,接着便会进行第二阶段confirm,去真正的执行业务(比如扣库存)。

在try的过程中如果发现有问题,则会触发另外一个第二阶段方法cancel,去释放在try阶段预留的资源(预留是什么意思,后面会讲到)。

因此对所有的参与者我们必须实现三个方法:

  • try:预留资源,确认资源可用。
  • confirm:使用预留资源,真正去执行业务。
  • cancel:取消预留资源。

大家可以看到网上很多都是这样写的,但是很多同学反馈上面的字都认识,但是连住一起就是不懂什么意思。我下面使用一个实际的例子,结合伪代码带大家走下整个实现流程。

不管是使用什么tcc框架(ByteTCC、Himly、Seata),只要是使用了tcc的方式,其中的原理都是一样的。接下来以Himly框架为例讲解。

2.1、 订单微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RequestMapping("/order")
public class OrderController {
//feign接口
@Autowired
private MemberFeign memberFeign;
//feign接口
@Autowired
private storageFeign storageFeign;

@PostMapping("add")
public ResultInfo add(Order order){
//第一步:生成订单
createOrder(order)
//第二步:减少库存(远程feign,调用/storage/reduce-num)
storageFeign.reduceNum(order);
//第三部:扣钱(远程feign)
memberFeign.reduceMoney(order);
}

//生成订单Try
@HmilyTCC(confirmMethod = "confirmCreateOrder", cancelMethod = "cancelCreateOrder")
public void createOrder(Order order){
//检查是否可以新增订单(是否满足活动条件)
}
//生成订单confirm
public void confirmCreateOrder(Order order){
//执行insert语句,新增订单
}
//生成订单cancel
public void cancelCreateOrder(Order order){
//执行delete语句,删除之前新增的订单
}
}

在第一步调用生成订单时,调用createOrder方法实际上是执行try,检查资源。在try方法上通过注解@HmilyTCC注解绑定了另外两个方法confirmCreateOrdercancelCreateOrder,分别对应confirmcancel。这两个方法会被tcc框架在合适的时机自行调用。对于confirm来说,需要等所有的try都执行完毕(也就是分布式事务发起者执行完毕),才会开始调用所有的confirm方法。

2.2、 仓库微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RequestMapping("/storage")
public class StorageController {

@PostMapping("reduce-num")
public ResultInfo reduce(Order order){
//减少库存
reduceNum(order);
}

//生成订单Try
@HmilyTCC(confirmMethod = "confirmReduceNum", cancelMethod = "cancelReduceNum")
public void reduceNum(Order order){
//1.检查是否有足够库存
//2.预留库存
//update t_storage set num = num-1 ,frozen_num = frozen_num+1 where id =?;
}
//生成订单confirm
public void confirmReduceNum(Order order){
//将冻结库存恢复 update t_storage set frozen_num = frozen_num-1 where id =?;
}
//生成订单cancel
public void cancelReduceNum(Order order){
//将真实冻结库存恢复
//update t_storage set num = num+1 , frozen_num = frozen_num-1 where id =?;
}
}

这里的try阶段做了2个重要的事情:

  1. 检查库存是否充足

  2. 把真实库存减掉,加到冻结库存中去。

为什么要减真实库存?

因为在第一阶段(try)扣掉的库存是不确定的,只有执行到第二阶段才能真正知道是不是该扣库存。因此如果try阶段不减真实库存,很有可能会超售

为什么要冻结库存?

cancel的时候,你可以通过冻结库存知道该如何恢复正确数量到真实库存。

2.3、 会员微服务

会员微服务的代码和仓库微服务差不太多,就不在赘述了。

3、一些问题

  • 冻结字段似乎无法处理多个人同时购买一个商品的情况。此时冻结库存会把多个人的库存量搞混。

  • TCC自身还有一些问题需要处理,如:幂等处理、空回滚和资源悬挂。

这些问题将在下期谈到如何解决。