@[toc]
说好了写 TienChin 项目的,最近这个分布式事务算是一个支线任务吧,今天是最后一篇,松哥再来一个短篇和小伙伴们总结一下分布式事务。
首先先说一个大原则:分布式事务能不用就不要用,毕竟这个用起来还是有一些麻烦的。当然,不用和不会用可是两码事。
学习分布式事务,有一些基础理论需要我们先来了解下。
本地事务是指将多条语句作为一个整体进行操作的功能,通过数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败,如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。也就是事务具有原子性,一个事务中的一系列操作要么全部成功,要么全部失败。一般来说,事务具有 4 个属性:
这四个属性通常称为 ACID 特性。
这块松哥之前专门录过相关的视频,这里就不再赘述了。
当我们的项目上了微服务之后,分布式事务就是一个比较常见的问题了,我们也会遇到很多相关的场景。
就拿我们前两天讲的商品下单的分布式事务的案例来说,像下面这样,一共有五个服务,架构如下图:
当用户想要下单的时候,调用了 bussiness 中的接口,bussiness 中的接口又调用了它自己的 service,在 service 中,通过 feign 调用 storage 中的接口去扣库存,然后再通过 feign 调用 order 中的接口去创建订单(order 在创建订单的时候,不仅会创建订单,还会扣除用户账户的余额)。
这三个操作,我们希望他们能够同时成功或者同时失败。然而如上图所示,三个微服务都有自己的 DB,这是三个完全不同的 DB,相当于三个不同的本地事务,按照传统的本地事务规则,我们显然是无法实现三个操作同时成功或者同时失败的。
想要实现 storage、order 以及 account 中的操作同时成功或者同时失败,就得考虑分布式事务了。
最后,我们再来看看分布式事务的概念:分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于的不同节点之上,数据库的操作执行成功与否,不仅取决于本地 DB 的执行结果,也取决于第三方系统的执行结果。而分布式事务就保证这些操作要么全部成功,要么全部失败。本质上,分布式事务就是为了保证不同数据库的数据一致性。
CAP 定理(CAP theorem),有时候又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
CAP 原则的精髓就是要么 AP,要么 CP,要么 AC,但是不存在 CAP。因为在分布式系统内,P 是必然的发生的,不选 P,一旦发生分区,整个分布式系统就完全无法使用了,这样的系统就太脆弱了。所以对于分布式系统,我们只能能考虑当发生分区错误时,如何选择一致性和可用性(选择一致性,意味着服务在某段时间内不可用,选择了可用性,意味着服务虽然一直可用但是返回的数据却不一致)。
而根据一致性和可用性的选择不同,开源的分布式系统往往又被分为 CP 系统和 AP 系统。
当一套系统在发生分区故障后,客户端的任何请求都被卡死或者超时,但是系统的每个节点总是会返回一致的数据,则这套系统就是 CP 系统,经典的比如 Zookeeper。
如果一套系统发生分区故障后,客户端依然可以访问系统,但是获取的数据有的是新的数据,有的还是老数据,那么这套系统就是 AP 系统,经典的比如 Eureka。
因为无法同时满足 CAP,所以又有了 BASE 理论,BASE 理论指的是:
BASE 理论的核心思想是即便无法做到强一致性,但应该采用适合的方式保证最终一致性。
BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。
事务有刚性事务和柔性事务之分。
刚性事务(如单数据库中的本地事务)完全遵循 ACID 规范,即数据库事务正确执行的四个基本要素:
柔性事务,主要就是只分布式事务了,柔性事务为了满足可用性、性能与降级服务的需要,降低一致性(Consistency)与隔离性(Isolation)的要求,遵守 BASE 理论:
当然,柔性事务也部分遵循 ACID 规范:
柔性事务有不同的分类,不过基本上都可以看作是分布式事务的解决方案:
先来说说 XA。
XA 是一种典型的两阶段提交(2PC,Two-phase commit protocol),而两阶段提交是一种强一致性设计,在两阶段提交中,一般会引入一个事务协调者的角色来协调管理各个事务参与者,例如我们之前文章中使用的 seata-server 其实是就是一个事务协调者。所谓的两阶段分别指的是准备和提交两个阶段。
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。
XA 规范描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。
XA 规范使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。
XA 规范在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库如 MySQL、Oracle、MSSQL 等都对 XA 规范提供了支持。
XA 事务的基础是两阶段提交协议。需要有一个事务协调者来保证所有的事务参与者都完成了准备工作(第一阶段)。如果协调者收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个 XA 事务中扮演的是参与者的角色,而不是协调者(事务管理器)。
MySQL 的 XA 事务分为内部 XA 和外部 XA。外部 XA 可以参与到外部的分布式事务中,需要应用层介入作为协调者;内部 XA 事务用于同一实例下跨多引擎事务,由 Binlog 作为协调者,比如在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部 XA 事务,只不过二进制日志的参与者是 MySQL 本身。 MySQL 在 XA 事务中扮演的是一个参与者的角色,而不是协调者。
XA 事务的特点是:
3PC 主要是为了弥补 2PC 的不足而产生的,2PC 有哪些不足呢?
3PC 则尝试解决 2PC 的这些问题。3PC 主要是把 2PC 中的第一阶段再次一分为二,这样 3PC 就有 CanCommit、PreCommit 以及 DoCommit 三个不同的阶段。不过 3PC 并不能解决 2PC 的所有问题,3PC 主要解决了单点故障问题,并且减少了阻塞。一旦事务参与者(分支事务)无法及时收到来自事务协调者的信息,那么分支事务会默认执行 commit,而不会一直持有事务资源并处于阻塞状态,不过这种机制也带来了新的问题,假设事务协调者发送了 abort 指令给各个分支事务,然而由于网络问题导致分支事务没有及时接收到该指令,那么分支事务在等待超时之后执行了 commit 操作,这样就和其他接到 abort 命令并执行回滚的分支事务之间存在数据不一致的情况。
我们来看看 3PC 的流程:
相反,如果有一个分支事务节点未完成 PreCommit 的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送 abort 请求,从而中断事务。
关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC 模式主要有如下一些优缺点:
优点:
缺点:
TCC 主要是两个阶段,步骤如下:
在我们之前的文章中,松哥也给大家举了 TCC 的例子了,这里就不再赘述了。
SAGA 最初出现在 1987 年 Hector Garcaa-Molrna & Kenneth Salem 发表的论文 SAGAS 里。这篇论文的核心思想是将长事务拆分为多个短事务,由 Saga 事务协调器协调,如果每个短事务都成功提交完成,那么全局事务就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 事务的特点是:
SAGA 适用的场景较多,适用于长事务或者对中间结果不敏感的业务场景。
本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章中提出。
顾名思义,本地消息表就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表以及业务肯定是一起执行成功的。
当一个操作执行成功之后,再去执行下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改为已成功;如果下一个任务调用失败也没关系,会有后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务(重试),服务更新成功了再变更消息的状态。
重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
根据上面的描述,小伙伴们其实可以看到,本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
本地消息表的特点:
根据本地消息表的特点我们可以发现,本地消息表适用于可异步执行且后续操作无需回滚的业务。
这种方案的核心思路,其实就是通过消息中间件来将全局事务转为本地事务,通过消息中间件来确保各个分支事务最终都能调用成功。
不过后来发现利用 Alibaba 的 RocketMQ(4.3之后)可以更好的实现分布式事务。
RocketMQ 是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像 2PC、3PC、TCC 那样强一致分布式事务,在 RocketMQ 中有一种消息叫做 Half Message,Half Message 是指暂不能被 Consumer 消费的消息,虽然 Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递状态,处于该种状态下的消息称为半消息,此时需要 Producer 对消息进行二次确认后,Consumer 才能去消费它。
RocketMQ 就是基于 Half Message 来实现的分布式事务,举一个转账的例子:
可能有小伙伴会说,那要是 B 最终执行失败怎么办?对于这种情况,我们几乎可以断定就是代码有问题所以才引起异常,因为消费端 RocketMQ 有重试机制,如果不是代码问题一般重试几次就能成功。
如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理,人工兜底处理后,就可以让事务达到最终的一致性。
发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
在前面两个小节介绍的的本地消息表和事务消息都属于可靠消息,这与我们这里介绍的最大努力通知有什么不同?
仅此而已。
在具体的解决方案上,最大努力通知需要消息发起方提供接口,让被通知方能够通过接口查询业务处理结果。
最大努力通知适用于业务通知类型,最常见的场景就是支付回调,支付服务收到第三方服务支付成功通知后,先更新自己库中订单支付状态,然后同步通知订单服务支付成功。如果此次同步通知失败,会通过异步脚步不断重试地调用订单服务的接口。
最大努力通知更多是业务上的设计,在基础设施层,可以直接使用二阶段消息,或者事务消息、本地消息表等来实现。
好啦,学习分布式事务解决方案,最大的感受就是:没有银弹!
前面的文章松哥也和大家聊了很多实际的解决方案,也录制了相应的分布式事务视频在 TienChin 项目中,欢迎一起探讨。