分布式事务的产生
随着系统功能的不断迭代,以及用户量增加带来的复杂度,会给我们的数据库和服务带来过高的负载和性能瓶颈。
为了解决性能瓶颈,我们可以先对存储层进行业务维度的分库,然后再对每个分库进行水平拓展来解决存储层的性能瓶颈,接着我们可以再对应用按照业务维度进行拆分。最终我们的一个单体应用会被拆成多个应用部署在不同的服务器上,每个应用有着自己的数据库。
什么是分布式事务?
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
对于分布式事务而言几乎满足不了 ACID,其实对于单机事务而言大部分情况下也没有满足 ACID,不然怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应用上的分布式事务了。
事务的ACID
原子性(Atomicity)
即事务最终的状态只有两种,全部执行成功和全部不执行。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。
一致性(Consistency)
是指事务操作前和操作后,数据的完整性保持一致或满足完整性约束。
比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加200 元 ; 一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况 (该情况,用户 A 和 B 均为 600 元,总共 1200 元)。
隔离性(Isolation)
是指当系统内有多个事务并发执行时,多个事务不会相互干扰,即一个事务内部的操作及使用的数据,对其他并发事务是隔离的。
持久性(Durability)
也被称为永久性,是指一个事务完成了,那么它对数据库所做的更新就被永久保存下来了。即使发生系统崩溃或宕机等故障,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。
柔性事务与刚性事务
刚性事务
遵循 ACID 原则,具有强一致性。比如,数据库事务。
柔性事务
其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致
哪些架构存在分布式事务
同服务不同数据库,不同服务不同数据库。
实现分布式事务的五种方法
DTP模型
DTP 规范中主要包含了 AP、RM、TM 三个部分,其中 AP 是应用程序,是事务发起和结束的地方;RM 是资源管理器,主要负责管理每个数据库的连接数据源;TM 是事务管理器,负责事务的全局管理,包括事务的生命周期管理和资源的分配协调等。
XA 则规范了 TM 与 RM 之间的通信接口,在 TM 与多个 RM 之间形成一个双向通信桥梁,从而在多个数据库资源下保证 ACID 四个特性。
数据库层面,保证数据的强一致性
1.基于 XA 协议的二阶段提交协议方法
XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。
事务管理器与资源管理器
事务管理器作为协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常由数据库实现,比如 Oracle、DB2 等商业数据库都实现了 XA 接口。
2PC
二阶段提交协议(The two-phase commitprotocol,2PC),用于保证分布式系统中事务提交时的数据一致性,是 XA 在全局事务中用于协调多个资源的机制。
2PC的两个阶段
投票 锁住资源但是不提交
- 应用程序发起事务请求
- 协调者TM发送canCommit给事务参与者RM,等待响应。
- 参与者收到后会执行请求中的事务,记录日志但是不提交,执行成功则发送yes,反之no。
提交
- 协调者收到所有参与者的返回后进入提交阶段,如果都是yes,则发送DoCommit给参与者。参与者执行剩余的操作并释放资源,返回HaveCommited。
- 如果有no,则发送DoAbort消息,参与者收到后进行回滚,然后发送HaveCommited给协调者。
- 协调者收到HaveCommited后就意味着整个事务结束了。
缺点
同步阻塞问题:
二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。
单点故障问题:
基于 XA 的二阶段提交算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
数据不一致问题:
在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
2.三阶段提交协议方法;
三阶段提交协议(Three-phase commit protocol,3PC),是对二阶段提交(2PC)的改进。为了解决两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段。
优化
同时在协调者和参与者中引入超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务。
过程
在第一阶段和第二阶段中间引入了一个准备阶段,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。
- CanCommit
协调者发送canCommit请求,询问是否可以执行事务操作,参与者回复yes或者no。 - PreCommit
如果都是yes,则发送preCommit请求给参与者,进入预提交阶段。 参与者收到preCommit之后执行事务,并记录Undo和Redo日志。 如果参与者执行事务成功,返回ACK,同时等待最终指令。 如果有No,或者等待超时之后,执行中断,发送Abort给参与者,参与者收到后执行中断操作。 - DoCommit
如果preCommit成功了,协调者接收到所有的Ack,进入提交状态,发送DoCommit命令给参与者,参与者提交事务后释放资源,提交完后发送Ack给协调者,当协调者收到所有Ack,则标记事务完成。
在 DoCommit 阶段,当参与者向协调者发送 Ack 消息后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,不会像两阶段提交那样被阻塞住。
Spring JTA 只能解决同一服务下操作多数据源的分布式事务问题,换到微服务架构下,可能存在同一个事务操作,分别在不同服务上连接数据源,提交数据库操作。
业务层面,保证最终一致性。
3.TCC
而 TCC 正是为了解决以上问题而出现的一种分布式事务解决方案。TCC 采用最终一致性的方式实现了一种柔性分布式事务,与 XA 规范实现的二阶事务不同的是,TCC 的实现是基于服务层实现的一种二阶事务提交。
TCC 分为三个阶段,即 Try、Confirm、Cancel 三个阶段。
Try 阶段:主要尝试执行业务,执行各个服务中的 Try 方法,主要包括预留操作;
Confirm 阶段:确认 Try 中的各个方法执行成功,然后通过 TM 调用各个服务的Confirm 方法,这个阶段是提交阶段;
Cancel 阶段:当在 Try 阶段发现其中一个 Try 方法失败,例如预留资源失败、代码异常等,则会触发 TM 调用各个服务的 Cancel 方法,对全局事务进行回滚,取消执行业务。
失败TCC 会不停地重试调用失败的 Confirm 或 Cancel 方法,直到成功为止。
缺点
对业务的侵入性非常大。首先,我们需要在业务设计的时候考虑预留资源;然后,我们需要编写大量业务性代码,例如 Try、Confirm、Cancel 方法;最后,我们还需要为每个方法考虑幂等性。实现和维护的成本非常高。
4.基于消息的最终一致性方法。
2PC 和 3PC 这两种方法,有两个共同的缺点,一是都需要锁定资源,降低系统性能;二是,没有解决数据不一致的问题。因此,便有了通过分布式消息来确保事务最终一致性的方案。
Work Flow
- 订单系统把订单消息发给消息中间件,消息状态标记为“待确认”。
- 消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为“待发送”的消息。
- 消息中间件返回消息持久化结果(成功 / 失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单。
- 订单操作完成后,把操作结果(成功 / 失败)发送给消息中间件。
- 消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为“待发送(可发送)”,并执行消息投递。
- 如果消息状态为“可发送”,则 MQ 会将消息发送给支付系统,表示已经创建好订单,需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。
- 订单系统支付完成后,会将支付消息返回给消息中间件,中间件将消息传送给订单系统。订单系统再调用库存系统,进行出货操作。
可以看出,分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以,分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。
5.业务无侵入方案——Seata(Fescar)
Seata 是阿里去年开源的一套分布式事务解决方案,开源一年多已经有一万多 star 了,可见受欢迎程度非常之高。
Seata 的基础建模和 DTP 模型类似,只不过前者是将事务管理器分得更细了,抽出一个事务协调器(Transaction Coordinator 简称 TC),主要维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。而 TM 则负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。如下图所示:
流程\
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚决议;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
提交阶段
Seata 与其它分布式最大的区别在于,它在第一提交阶段就已经将各个事务操作 commit了。Seata 认为在一个正常的业务下,各个服务提交事务的大概率是成功的,这种事务提交操作可以节约两个阶段持有锁的时间,从而提高整体的执行效率。
原子性和一致性
失败回滚
Seata 将 RM 提升到了服务层,通过 JDBC 数据源代理解析 SQL,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交。
如果 RM 决议要全局回滚,会通知 RM 进行回滚操作,通过 XID 找到对应的回滚日志记录,通过回滚记录生成反向更新 SQL,进行更新回滚操作。
隔离性
seata认为大多数分布式业务涉及到脏读的可能性比较小,所以保证了大多数场景下的高效性。
Seata 设计通过事务协调器维护的全局写排它锁,来保证事务间的写隔离,而读写隔离级别则默认为未提交读的隔离级别。
对比