分布式事务
分布式事务的产生
我们先看看百度上对于分布式事务的定义:分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
当系统的体量很小时,单体架构完全可以满足现有业务需求,所有的业务共用一个数据库,整个下单流程或许只用在一个方法里同一个事务下操作数据库即可。此时做到所有操作要么全部提交 或 要么全部回滚很容易。
可随着业务量的不断增长,单体架构渐渐扛不住巨大的流量,此时就需要对数据库、表做 分库分表
处理,将应用 SOA
服务化拆分。也就产生了订单中心、用户中心、库存中心等,由此带来的问题就是业务间相互隔离,每个业务都维护着自己的数据库,数据的交换只能进行 RPC
调用。
当用户再次下单时,需同时对订单库 order
、库存库 storage
、用户库 account
进行操作,可此时我们只能保证自己本地的数据一致性,无法保证调用其他服务的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。
分布式事务解决方案
强一致性分布式方案
强一致分布式事务方案:其中包括两段式提交协议2PC
、三段式提交协议3PC
。
关于二段式和三段式可以戳这里了解:分布式系统中的数据一致性问题
尽管2PC/3PC
存在一些问题,但其实是通过提升服务运营能力部分克服问题,那是不是2PC/3PC
就可以满足微服务场景下分布式事务的需求了呢?答案是否定的,原因有三点:
- 由于微服务间无法直接进行数据访问,微服务间互相调用通常通过
RPC(Dubbo)
或Http API(Spring Cloud)
进行,所以已经无法使用TM(Transaction Manager)统一管理微服务的RM(Resource Manager)。 - 不同的微服务使用的数据源类型可能完全不同,如果微服务使用了
NoSQL
之类不支持事务的数据库,则事务根本无从谈起。 - 即使微服务使用的数据源都支持事务,那么如果使用一个大事务将许多微服务的事务管理起来,这个大事务维持的时间,将比本地事务长几个数量级。如此长时间的事务及跨服务的事务,将为产生很多锁及数据不可用,严重影响系统性能。
由此可见,传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。
关于Base理论:分布式系统CAP定理与BASE理论
BASE中的最终一致性是对于微服务下的事务管理的根本要求,即虽然基于微服务的事务管理无法达到强一致性,但必须保证最终一致性,这就是所说的柔性事务。
最终一致性分布式方案
最终一致分布式事务方案:其中包括事件通知模式(本地异步事件服务模式、外部事件服务模式、事务消息模式、最大努力通知模式)、事务补偿模式(Saga
、TCC
)。
实现事务最终一致性的方案主要有事件通知模式、事务补偿模式两种。
事件通知模式
事件通知模式的设计理念比较容易理解,即是主服务完成后将结果通过事件(常常是消息队列)传递给从服务,从服务在接受到消息后进行消费,完成业务,从而达到主服务与从服务间的消息一致性。
本地异步事件服务模式
为了解决上述同步事件中描述的同步事件的问题,异步事件通知模式被发展了出来,既业务服务和事件服务解耦,事件异步进行,由单独的事件服务保证事件的可靠投递。
事务补偿模式
TCC
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC 事务机制相比于上面介绍的 XA,解决了其几个缺点:
-
解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
-
同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
-
数据一致性,有了补偿机制之后,由业务活动管理器控制一致性
Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
在 Try 阶段,是对业务系统进行检查及资源预览,比如订单和存储操作,需要检查库存剩余数量是否够用,并进行预留,预留操作的话就是新建一个可用库存数量
字段,Try 阶段操作是对这个可用库存数量进行操作。
基于 TCC 实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口,所以代码实现复杂度相对较高。