目录
一、前言
微服务架构的大规模应用随之带来的一个问题就是传统的单体事务演化为分布式事务,因为各个微服务都将拥有自己独立的数据库,以springcloud技术栈的架构来说,微服务间互相调用时,当前的服务调用方无法保证被调用方的事务处理是否一定能成功,这就是分布式事务的问题起源。
二、事务简介
为了更好的理解分布式事务原理,再次回顾下事务的几个特性
事务的4个特性:原子性、一致性、隔离性、持久性,这四个属性通常称为ACID特性
2.1 原子性
一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做
2.2 一致性
事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的
2.3 隔离性
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
隔离性又分为四个级别:
- 读未提交(read uncommitted);
- 读已提交(read committed),解决脏读问题;
- 可重复读(repeatableread),解决虚读问题;
- 串行化(serializable),解决幻读问题;
2.4 持久性
持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
小结
任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
三、分布式事务场景
3.1 分布式事务起源
如下一个场景,订单服务的逻辑中需要创建一个订单,需要同时调用3个操作db的方法,如果放在单体应用中,并且共同使用一个库的情况下,在一个单体事务中可以完成全部操作,只需通过事务注解即可。
随着架构的演进和升级,单体架构已不能满足高并发、高吞吐的要求了,必然面临着拆库,微服务体系下的架构将会演变成下面这样,
从上面的调用链路来看,很明显,在分布式环境下,各个服务应用都是独立的数据库,在这种情况下,为了创建一个订单,需要在订单的逻辑中调用多个微服务的接口(这里只考虑了调用3个服务接口),假如说当创建订单调用短信服务失败,这将涉及到回滚操作,而订单创建的逻辑只能确保订单库本身的数据完整性,无法保证短信数据是否能正常回滚,这就带来了分布式事务的问题。
在一些大型的分布式应用中,一个业务中调用4个5个服务的很常见,因此在微服务场景下,分布式事务是一个很难回避的问题。
3.2 分布式事务典型场景
通过上面的例子,可以得出这样的信息,在微服务场景下,通常完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
通常来说,分布式事务的场景大概有下面这些
3.2.1 跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。上面的案例即是涉及到了跨库事务,下图演示了一个服务同时操作2个库的情况:
3.2.2 分库分表
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:
当进行分库分表之后,以前在单库情况下的一条sql语句,比如:insert into user(id,name) values (1,"张三"),(2,"李四"),这种完全可以通过数据库本身的特性保证事务的一致性,但是做了分库分表后,这两条数据将会按照一定的规则被插入到不同的数据库中,由于是不同的数据库,将无法保证两条sql一定能够执行成功。
3.2.3 服务化
微服务架构是目前运用主流的服务化解决方案,服务化之后,各个微服务将拥有自己独立的数据库,各个微服务之间将通过RPC框架进行远程的调用,实现彼此的通信,下图将再次还原上面案例中的服务间调用的场景
小结
上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
四、分布式事务常用解决方案
基于分布式事务问题的现状,随着微服务架构的应用越来越成熟,也出现了不少可以落地实践的解决方案,比如:
- seata ,阿里开源的分布式事务框架;
- 消息队列解决方案,比如rocketmq事务消息机制;
- SAGA;
- XA;
- ...
他们有一个共同点,都是“两阶段(2PC)”协议的实现,两阶段是指完成整个分布式事务,划分成两个步骤完成。
实际上,这四种常见的分布式事务解决方案,分别对应着分布式事务的四种模式:AT、TCC、Saga、XA四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出。同时,每种模式都有它的适用场景,同样每个模式也都诞生有各自的代表产品,而这些代表产品,可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC)。因此在具体学习某个分布式事务解决方案之前,有必要系统了解下分布式事务的理论基础。
4.1 分布式事务理论基础
分布式事务相关的协议有2PC、3PC。由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。
4.1.1 2PC两阶段提交协议
顾名思义,分为两个阶段:Prepare 和 Commit
2PC执行流程如下所示:
4.1.2 prepare阶段
prepare为提交事务请求阶段,按照上图来说,其主要的执行流程包括如下几步:
1. 询问, 协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。
2. 执行 ,各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将 Undo 和 Redo 信息记录事务日志中。
3. 响应, 如果参与者成功执行了事务并写入 Undo 和 Redo 信息,则向协调者返回 YES 响应,否则返回 NO 响应。当然,参与者也可能宕机,从而不会返回响应
4.1.3 commit 阶段
commit阶段为执行事务提交阶段,包括正常提交或回退
正常提交
按照上图,其主要的执行流程包括如下几步:
1. commit 请求 协调者向所有参与者发送 Commit 请求;
2、事务提交 参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源;
3、反馈结果 参与者执行事务提交后向协调者发送 Ack 响应;
4、完成事务 接收到所有参与者的 Ack 响应后,完成事务提交
事务中断
在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无法收到所有参与者的 YES 响应,或者某个参与者返回了 No 响应,此时,协调者就会进入回退流程,对事务进行回退。
流程如下图红色部分(将 Commit 请求替换为红色的 Rollback 请求):
按照上图,其主要的执行流程包括如下几步:
1、rollback 请求 协调者向所有参与者发送 Rollback 请求;
2、事务回滚 参与者收到 Rollback 后,使用 Prepare 阶段的 Undo 日志执行事务回滚,完成后释放事务执行期占用的所有资源;
3、反馈结果 参与者执行事务回滚后向协调者发送 Ack 响应;
4、中断事务 接收到所有参与者的 Ack 响应后,完成事务中断;
4.1.4 两阶段问题
事实上,2PC协议在实践过程中也会引入其他问题,这里总结如下几点
同步阻塞
参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。 倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。
单点问题
在 2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长等),所以也无法顺利处理上一个事务。
数据不一致
Commit 事务过程中 Commit 请求/Rollback 请求可能因为协调者宕机或协调者与参与者网
络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback 请求,而其他参与者则正常收到执行了 Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。当参与者执行 Commit/Rollback 后会向协调者发送 Ack,然而协调者不论是否收到所有的参与者的 Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成功。
环境可靠性依赖
协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络
中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
五、分布式事务Seata
5.1 Seata介绍
5.1.1 Seata是什么
Seata 是一款阿里巴巴团队开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
5.1.2 为什么选择Seata
- 分布式事务存在CAP的难题
- 阿里团队开源,全世界程序员维护,有技术保障
- 成熟的分布式应用,众多的大厂使用案例
- 提供多种不同的分布式事务实现模式(AT,TCC,Aaga)
5.2 Seata常用几种分布式事务模式
seata提供了常用的几种分布式事务实现的模式,接下来分别介绍下
5.2.1 AT模式
AT 模式是一种无侵入的分布式事务解决方案,Seata实现了该模式,在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
AT 模式如何做到对业务的无侵入,根据上图来看,总结如下:
一阶段
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数
据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,
在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库
事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一
阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方
式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
5.2.2 TCC模式
TCC 模式属于二阶段协议实现的一种,需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法,该模式的使用,需要注意下面两点:
1. 侵入性比较强, 并且得自己实现相关事务控制逻辑;
2.在整个过程基本没有锁,性能更强;
TCC 整个执行过程如下图所示:
在TCC模式中,有3个重要的方法
- try,资源的检测和预留;
- Confirm,执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
- Cancel:预留资源释放;
由于在上文对两阶段提交模式做了详细的解读,这里就不再过多赘述了。
5.2.3 SAGA模式
saga模式的实现,是长事务解决方案。
Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。如下图:T1~T3都是正向的业务流程,都对应着一个冲正逆向操作C1~C3
SAGA模式主要特点
- 分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交;
- 如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态;
- Saga 正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的;
- Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案;
Saga 模式使用场景
1)Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。
2)事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,可以使用 Saga 模式。
Saga 模式优点
1)一阶段提交本地数据库事务,无锁,高性能;
2)参与者可以采用事务驱动异步执行,高吞吐;
3)补偿服务即正向服务的“反向”,易于理解,易于实现;
Saga 模式缺点
Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。
与TCC实践经验相同的是,Saga 模式中,每个事务参与者的冲正、逆向操作,需要支持,即:
- 空补偿:逆向操作早于正向操作时;
- 防悬挂控制:空补偿后要拒绝正向操作;
- 幂等;
5.2.4 XA模式
XA是X/Open DTP组织(X/Open DTP group)定义的两阶段提交协议,XA被许多数据库(如
Oracle、DB2、SQL Server、MySQL)和中间件等工具(如CICS 和 Tuxedo)本地支持 。
关于XA做如下几点说明
1、XA接口函数由数据库厂商提供。XA规范的基础是两阶段提交协议2PC;
2、JTA(Java Transaction API) 是Java实现的XA规范的增强版 接口;
在XA模式下,需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协调器,把结果给协调器。协调器等所有分支事务操作完成、都预提交后,进行第二步;第二步:协调器通知每个数据库进行逐个commit/rollback,其中,这个全局协调器就是XA模型中的TM角色,每个分支事务各自的数据库就是RM。
MySQL 提供的XA实现(https://dev.mysql.com/doc/refman/5.7/en/xa.html ),XA模式下的 开源框架有atomikos,其开发公司也有商业版本。
XA模式缺点
事务粒度大。高并发下,系统可用性低。因此很少使用
5.2.5 四种模式分析
AT、TCC、Saga、XA四种不同的分布式事务模式, 分别在不同的时间被提出,每种模式都有它的适用场景,
- AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本;
- TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景;
- Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 ;
- TCC 要求的接口,也可以使用 Saga 模式;
- XA模式是分布式强一致性的解决方案,但性能低而使用较少;
总结
分布式事务本身就是一个技术难题,业务中具体使用哪种方案还是需要不同的业务特点自行选择,但是我们也会发现,分布式事务会大大的提高流程的复杂度,会带来很多额外的开销工作,代码量上去了,业务复杂了,性能下跌了。所以,当我们真实开发的过程中,能不使用分布式事务就不使用
5.3 Seata 三大角色
在编码之前,有必要对Seata中的相关术语做一个全面的了解何学习,以免后面云里雾里。
在 Seata 的架构中,一共有三个角色:
TC
Transaction Coordinator,事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM
(Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM
(Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。其中,TC 为单独部署的 Server 服务端TM 和 RM 为嵌入到应用中的 Client 客户端。
在 Seata 中,一个分布式事务的生命周期如下:
结合上图,具体执行流程如下
1、TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起,当一进入事务方法中就会生成XID , global_table 就是用来存储全局事务信息;
2、RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。当运行数据库操作方法,branch_table 存储事务参与者;
3、TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚;
4、TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚;
5.4 Seata AT模式下的实现流程
结合上面官网关于Seata实现的流程,这里详细介绍下Seata的AT模式下分布式事务实现的完整流程。官方文档:官方文档说明
AT模式的核心是对业务无侵入,是一种改进后的两阶段提交,其设计思路如图
5.4.1 第一阶段
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务sql进行解析,转换成undolog,并同时入库,这是怎么做的呢?先抛出一个概念DataSourceProxy代理数据源,通过名字大家大概也能基本猜到是什么个操作,后面做具体分析。
5.4.2 第二阶段
分布式事务操作成功,则TC通知RM异步删除undolog
分布式事务操作失败,TM向TC发送回滚请求,RM 收到协调器TC发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
5.4.3 整体流程
如果用一段伪代码来描述,则可以描述如下
business 服务 createOrder {
库存服务 - 扣减库存
积分服务 - 增加积分
}
从代码上来说,实现步骤如下
- TM端通过注解@GlobalTransactional进行全局事务的开启、提交、回滚;
- RM端seata通过扩展DataSource完成对DataSource的代理曾倩即DataSourceProxy,自动实现undo_log与TC上报;
- TC端通过seata-server实现(即一个java服务,可以从官网下载);
5.4.4 Seata 优势
相比与其它分布式事务框架,Seata架构亮点主要有几个:
1、应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
2、将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;
3、通过全局锁实现了写隔离与读隔离。
5.4.5 Seata 存在的问题
性能损耗
一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而
且是同步的。另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间。
性价比评估
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?
全局锁
相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。
回滚锁时释放时间长
Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。
死锁问题
Seata的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。
六、写在文末
本文通过较大的篇幅详细介绍了分布式事务框架seata的理论知识,这对于真正使用seata做技术整合以及了解其他分布式事务框架还是很有必要的,希望对看到的小伙伴有用。