分布式事务解决方案汇总
前言
一般来说,涉及到多数据源的事务,我们称之为分布式事务。常见的分布式事务解决方案有以下几种:
- 2PC(两阶段提交)
- 3PC(三阶段提交)
- 可靠消息队列
- TCC
- SAGA事务
- AT事务
下面我们来看看这些解决方案是怎么解决分布式事务问题以及这些解决方案各有什么优缺点。
2PC(两阶段提交)
两阶段提交就是将事务提交拆成了准备阶段
和提交阶段
这两个阶段。
那怎么协调
准备阶段
和提交阶段
这两个阶段呢?这里我们需要引入协调者
的概念,协调者
就相当于一个总的事务管理器,它会根据事务的执行状态来判断事务最终是提交
还是回滚
。
准备阶段
,这一阶段协调者会询问事务的所有参与者是否执行完sql(注意:这个时候没有真正提交事务),如果已准备好提交回复Prepared
,否则回复Non-Prepared
,这个阶段仍然会持有数据库锁。
提交阶段
,如果全部的事务参与者
都返回给协调者
的是Prepared
,则协调者
向所有的参与者
发送Commit
指令,所有事务参与者
立即执行提交操作。 如果任意一个参与者
回复了Non-Prepared
消息,或任意一个参与者超时未回复,协调者
向所有参与者发送 Abort 指令,参与者
立即执行回滚操作。
两阶段提交的原理简单,缺点如下:
- 协调者单点问题:若协调者宕机,那所有的参与者必须一直等待。
- 性能问题:要等待所有的参与者处理完毕为止,性能较差(木桶原理)。
- 一致性问题:协调者如果因为网络问题或者宕机了,向一部分参与者发送了commit,另一部分还没来得及发就宕机了,就会造成数据不一致。
3PC(三阶段提交)
3PC
是将2PC
的准备阶段又细分成了CanCommit
和PreCommit
两个阶段。新增的CanCommit
是一个询问阶段,让参与者根据条件判断该事务是否能顺利完成(比如扣减库存,参与者判断库存是否足够)。
3PC
解决了2PC
的单点问题,怎么做的呢?
如果协调者在PreCommit
阶段开始了之后发生宕机,参与者没有收到DoCommit
的消息,3PC
默认的策略是将事务提交而不是回滚事务或者继续等待,这就避免了协调者的单点问题。
性能上,如果在事务能够正常提交的场景中,因为3PC
多了一个询问阶段,所以性能反而会更差。如果事务需要回滚的场景中,三阶段的性能通常要好一些。
一致性问题并没有得到解决,在事务需要回滚的场景下,如果因为网络问题参与者一直没有收到协调者的的Abort回滚指令,这些参与者将会错误的提交事务,仍有数据不一致的问题。
可靠消息队列
可靠消息队列是分布式事务的一种最终一致性的解决方案。比如我们在创建订单后扣减库存,涉及到订单服务和库存服务。订单服务创建好订单后,需要发送消息到库存服务扣减库存。我们需要保证:
- 创建订单后,订单服务发送扣减库存消息成功
- 库存服务接收到消息,消费成功
像市场上流行的 RocketMq
自带了事务消息。
它是如何保证创建订单和发送扣减库存消息一致呢? 它是基于两阶段提交和事务状态回查来实现的。
RocketMq 有个半事务状态的消息,这个状态的消息是不能消费者消费的。我们先发送半事务状态的消息,然后创建订单,订单创建完成后将该消息改为 COMMIT 状态,就可以保证订单的创建和发送扣减库存的消息一致。
如果由于网络原因或者 producer
宕机了,该半事务消息一直未被确认,消息服务器会对该消息进行回查,来确定该消息是否应该提交。多久时间回查、回查多少次这些都是我们可以配置的,如果我们回查了一定次数还没有处理好该消息,我们可以设置报警,通知相关人员进行处理。
那消费端如何保证一定能消费成功呢?
如果消费端消费成功,我们会返回 CONSUME_SUCCESS
,如果消费出现异常,我们可以返回 RECONSUME_LATER
,让消息来重试,默认最多重试16次。如果重试了指定次数还没有消费成功,我们也可以通知相关人员进行相应的处理。
用事务消息来解决分布式事务的优点是可以实现应用之间的解耦,实现简单,容易落地。
缺点有如下几点:
- 代码层面需改造成事务消息的二阶段提交模式,由于有回查和重试,需保证接口的幂等性
- 事务的实时性由消息机制决定,可能会存在消息堆积问题
- 事务跨度大,各服务收到消息,就提交自己的事务,没有整体事务的隔离性。
TCC
上面说到事务消息没有隔离性,这里说到的 TCC
方案,就适合用于需要强隔离性的分布式事务中。
TCC
的操作有点复杂和麻烦,它是一种业务侵入性较强的方案,要求业务处理过程中必须拆分为 预留业务资源
和 确认/释放资源
两个子过程。它的实现过程分为三个阶段:
- Try : 尝试执行阶段,并预留好事务需要用到的所有资源(比如冻结可用库存)。
- Confirm : 确认执行阶段,无需进行业务检查,直接使用Try阶段准备的资源。该阶段可能会重复执行,因此需要满足幂等性。
- Cancel : 取消执行阶段,释放Try阶段预留的业务资源。该阶段也可能会重复执行,因此也需要满足幂等性。
如果将2PC
的提交阶段的提交和回滚拆开,你会发现和TCC
非常类似。但是TCC
是位于用户代码层面,而不是基础设施层面,我们就需要投入更高的开发成本。
优点:
- 隔离性强
- 在业务执行的时候,只操作预留资源,几乎不会涉及到资源的争用,所以性能较高
缺点:
- 业务侵入性高,开发成本高
- 技术不可控。如果你对接的有第三方服务(比如银行),别人不愿意按照TCC的方式来修改代码,那就推动不下去了。
针对第二种方式,我们可以考虑采用下面的柔性事务方案:SAGA
事务。
SAGA事务
SAGA
事务把大事务拆分成若干个子事务,每个子事务都可以被看作原子行为。
我们需要对每个子事务设计对应的补偿动作。
如果各个子事务都能提交成功,那么事务就可以顺利完成,否则我们就要采取以下两种恢复策略之一:
- 正向恢复:如果某个子事务提交失败,则一直对该事务进行重试,直至成功为止,这种适用于事务最终都要成功的场景。
- 反向恢复:如果某个子事务提交失败,则对该子事务及其之前所有的子事务进行补偿操作。
与TCC
相比,SAGA
不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往要比冻结操作容易实现的多。
缺点:
- SAGA必须保证所有子事务都能提交或者补偿,但是SAGA系统本身也可能会崩溃,所以它必须设计成与数据库类似的日志机制。
- 需要保证正向、反向恢复过程能严谨执行。
SAGA
是基于数据补偿来代替回滚的思路,也可以应用在其他事务方案上。比如阿里的 Seata
框架的 AT事务模式就是这样的一种应用。
AT事务
AT
事务也是参照两阶段提交协议来实现的,针对两阶段提交涉及到的资源必须都等到最慢的事务完成后才能统一释放的问题,AT
事务也设计了对应的解决方案。
它在业务数据提交时,自动拦截所有的SQL,分别保存SQL对数据修改前后的快照,这就相当于记录了重做和回滚日志。
如果分布式事务成功了,那我们后续只需清理每个数据源中对应的日志数据即可。如果分布式事务需要回滚,就要根据日志数据自动产生用于补偿的逆向SQL。
基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放资源,相比两阶段提交,极大的提升了系统的吞吐量。但是它和我们使用的事务消息一样,牺牲了隔离性。
总结
本文中提到的分布式事务解决方案有:
- 2PC(两阶段提交)
- 3PC(三阶段提交)
- 可靠消息队列
- TCC
- SAGA事务
- AT事务
根据上面的分析我们可以看到每种解决方案都有各自的优缺点,分布式事务是一个复杂的技术问题,并没有通用的解决方案,我们只能因地制宜,根据不同的场景来选择不同的解决方案。
好了,这篇文章就到这里了,感谢大家的观看!如有错误,请及时指正!欢迎大家关注我的公众号:贾哇技术指南
。