解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源的框架来解决分布式事务问题。
在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的Seata了。
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata 可能给是目前已知最可靠的分布式事务解决方案。
官网地址:Seata | Seata,其中的文档、播客中提供了大量的使用说明、源码分析。
其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态,因此解决分布式事务的思想非常简单:
Seata也不例外,在Seata的事务管理中有三个重要的角色:
- TC(Transaction Coordinator)- 事务协调者:维护全局和分支事务的状态,协调驱动全局事务提交或回滚。
- TM(Transaction Manager)- 事务管理器:定义全局事务的范围、开启一个全局事务、提交或回滚一个全局事务。
- RM(Resource Manager)- 资源管理器{事务参与者}:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 => 执行每个事务分支的操作。
作为一个分布式事务,它肯定也会有一个入口方法,在这个入口方法当中,一定会去调用多个其它的微服务,每调一个微服务,这个微服务不就是一个分支事务,因此将来调了多少个微服务,将来我们这个全局事务就包含多少个分支事务,因此在这个入口方法里就定义了全局事务的范围了,而TM就会去监控这个入口的方法或者说是代理这个入口方法,这样一来,TM就知道了全局事务里面总共有多少个分支事务,整个范围就确定下来了,当入口方法被执行时,TM会首先拦截当前的这个执行,会去向TC发起一个请求,去注册全局事务,接下来,就可以去执行这个入口的业务逻辑了,去调用每一个微服务,到了微服务里面每个分支事务就要开始执行了,此时RM就排上用场了。。。
其中,TC 为单独部署的 Server 服务端,TC服务则是事务协调中心,是一个独立的微服务,是一个独立的JVM进程,里面不包含任何业务代码,需要单独部署;TM 和 RM 为嵌入到应用中的 Client 客户端,引入到参与事务的微服务依赖中即可,将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
- TM 请求 TC 开启一个全局事务,TC 会生成一个 XID 作为该全局事务的编号。
XID - 全局事务ID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
- RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
- TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。
- TC 驱动 RM 将 XID 对应的自己的本地事务进行提交还是回滚。
参与分布式事务的每一个微服务都需要集成Seata,为了方便各个微服务集成Seata,我们需要把Seata配置共享到Nacos,因此模块不仅仅要引入Seata依赖,还要引入Nacos依赖。
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-starter-bootstrap
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
将原来的@Transactional注解改为Seata提供的@GlobalTransactional注解,@GlobalTransactional注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。
- XA模式:强一致性分阶段事务模型,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
无论哪种方案,都离不开TC,也就是事务的协调者。
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
第一阶段 - Prepare准备阶段:执行各个本地事务,即执行分支事/业务sql,但不提交
- TC事务协调者通知每个RM事物参与者执行本地事务
- 本地事务执行完成后报告事务执行状态给TC事务协调者,此时事务不提交,继续持有数据库锁
二阶段 - Commit或Rollback提交阶段:提交各个本地事务
TC事务协调者基于一阶段的报告来判断下一步操作:
- 如果一阶段都成功,则通知所有RM事务参与者,提交事务
- 如果一阶段任意一个参与者失败,则通知所有RM事务参与者fallback回滚事务
XA模式的优点:
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点:2PC存在的问题
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差;
- 依赖关系型数据库实现事务
- 同步阻塞问题: 各个事务在未提交时属于阻塞状态,其它事务想要操作行数据只能等待
- 网络分区导致的数据不一致问题:例如A、B两个本地事务,二阶段时一个提交成功,一个由于网络原因提交失败,解决方案:可设置统一的提交或者回滚,或者感知到异常做兜底业务处理
- 单点故障问题:各个本地事务的提交或回滚全部由XA的Server端控制,Server端一旦宕机各个本地事务都会处于阻塞状态,解决方案:做集群,但集群只能保证重新选出新的Server端控制者,本地事务阻塞的问题是无法解决的
- 一阶段:测试网络通信,并且询问各个本地事务是否进行事务操作
- 二阶段:执行各个本地事务操作,但不提交
- 三阶段:提交各个本地事务,执行commit或者rollback
由于三阶段提交协议3PC非常难实现,目前市面主流的分布式事务解决方案都是2PC协议。
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
1. 修改每个参与事务的微服务的application.yml文件,开启XA模式:
seata: data-source-proxy-mode: XA #开启数据源代理的XA模式
2. 给发起全局事务的入口方法添加@GlobalTransactional注解
3. 重启服务并测试
在AT模式下,用户只需关注自己的"业务SQL",用户的"业务SQL"作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
一阶段:
一阶段本地事务提交前,需要确保先拿到全局锁:
二阶段:
第二阶段根据阶段一的结果来判断
二阶段提交:大多数情况下全局事务都是执行成功
二阶段回滚:回滚通过一阶段的回滚日志进行反向补偿
- 在Seata内部,全局锁的重试它是有一个限制的,默认是30次,间隔10毫秒,也就是最多等待300毫秒,超出范围则放弃,并回滚本地事务,释放本地DB数据库锁。
- 为了提高性能,AT模式是基于2PC的,但2PC的一阶段并不会真正提交本地事务,也就意味着一直持有资源不释放,性能较差;
- 但AT模式在一阶段提交了本地事务,其它本地事务就可以执行,但全局锁还没有释放吗,同时还有undo_log image的记录,来保证后续有异常仍然能够进行事务回滚,提高性能。
- 读未提交,因为当一阶段本地事务提交之后,其它的分布式事务已经看到提交海购的数据,但因为有全局锁的存在,此时全局锁还没有释放,只能读到数据不能写数据。
AT模式的优点:
- 一阶段直接完成事务的提交,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- AT模式的一阶段、二阶段提交和回滚均由Seata框架自动生成,用户只需编写"业务SQL",便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案 => 没有代码侵入,框架自动完成回滚或提交。
- 可见,AT模式使用起来更加简单,无业务侵入,性能更好,因此企业90%的分布式事务都可以用AT模式来解决。
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
- XA模式一阶段不提交事务,锁定资源;而AT模式一阶段直接提交,不锁定资源;
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚;
- XA模式强一致,AT模式弱一致(最终一致);
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复
TCC模式,全称Try-Confirm-Cancel,通过名称也能看出来其流程主要有三个步骤 - 两个阶段,TCC实际上是服务化的两阶段提交协议:
- 预处理Try:实现业务检查(一致性)和资源预留(隔离)=> 比如冻结金额
- 确认 / 提交Confirm:做业务确认和提交操作 => 要求Try成功Confirm一定要能成功
- 撤销 / 回滚Cancel:Cancel阶段是在业务执行操作需要回滚的状态下执行分支事务的业务取消操作,预留资源释放,Cancel实现一个与Try相反的操作,即业务回滚操作
- TM事务管理器首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM事务管理器将会发起所有分支事务的Cancel执行分支事务的业务取消操作,若try操作全部执行成功,TM事务管理器将会发起所有分支事务的Confirm操作,其中Confirm / Cancel操作若执行失败,TM会进行重试。
- 通常情况下,采用TCC则认为Confirm阶段是不会出错的,即:只要Try成功,Confirm一定成功,若Confirl阶段真的出错了,需要引入重试机制或人工处理。
- 通常情况下,采用TCC则认为Cancel阶段也是一定成功的,在TCC中,Try和Confirm失败了都要调用Cancel,若Cancel阶段真的出错了,需引入重试机制或人工处理。
一般有以下几种处理手段:
- 记录日志&发送报警:将错误信息记录下来,方便后续分析和处理,并及时通知相关人员进行处理
- 自动重试:在一定程度上,可以通过自动重试的方式尝试多次执行Cancel操作,直到成功为止
- 人工干预:如果重试多次还是不成功,可以报警,然后进行人工干预,可以尝试手动执行Cancel擦偶哦或者进行数据修复等
分支事务成功的情况:
分支事务失败的情况 :
- TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用组件,是为了考虑结构和软件复用。
- TM事务管理器在发起全局事务时会生成全局事务记录,全局事务ID - XID会贯穿整个分布式事务调用链条,用户记录事务上下文、追踪和记录状态,由于Confirm和Cancel失败需要进行重试,因此需要将接口实现为幂等性,幂等性是指同一个操作无论请求多少次,其结果都相同。
- 整个实现TCC的过程,实际上是没有用到全局锁的,这是和AT模式的一个大的区别,TCC模式更多是利用本地行锁或者乐观锁、状态区分的形式来实现资源隔离,另外,相比AT模式,也无需生成数据快照 => TCC模式比AT模式性能要高很多,因为没有全局锁的限制,所以其速度飞快提升,因此TCC模式也适用于对性能有较高的分布式事务场景
- TCC是基于业务层面的,三个阶段的操作都需要自己编写代码来实现,衍生出来的问题就是跨部门协调很麻烦
- TCC模式需要自己实现代码,因此相比AT模式,TCC模式对业务代码有一定的侵入性
- TCC模式无需依赖于数据库事务性,而是依赖补偿操作,因此可以用于非事务型数据库
- TCC模式有软状态,事务是最终一致
- TCC模式适用于对性能有较高要求的业务场景
- TCC模式支持跨服务,而Seata的AT模式是不能跨服务的,但是因为TCC的二阶段代码都是我们自己实现的,所以跨服务跨平台都可以自主实现
- TCC模式需要考虑Confirm、Cancel接口的幂等性设计
蚂蚁金服TCC实践,总结以下注意事项:
➢ 业务模型分2阶段设计
➢ 并发控制
➢ 允许空回滚
➢ 防悬挂控制
➢ 幂等控制
用户接入TCC,最重要的是考虑如何将自己的业务模型拆成两阶段来实现,实现TCC的三个方法:
- 以"扣钱"的的场景为例,在接入TCC之前,对A账户的扣钱,只需要一条update更新账户余额的SQL语句便能完成;但是在接入TCC之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆分成两阶段,实现TCC的三个方法,并且保证一阶段Try成功的话,二阶段Confirm一定能成功。
- 在没有调用TCC资源的Try方法的情况下,来调用二阶段的Cancel方法,因此Cancel方法需要识别出来这是一个空回滚,然后直接返回成功,让事务管理器TM认为已回滚,否则会不断重试,而Cancel又没有对应的业务数据可以进行回滚,可能导致Cancel一直失败,最终导致整个分布式事务失败。
- 出现原因就是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
- 解决思路的关键就是要识别出来这个空回滚,思路很简单,就是需要知道一阶段是否执行,如果执行,那就是正常回滚;如果没执行,那就是空回滚,可以再额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入一条记录,表示一阶段执行了,Cancel接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。
- 悬挂的意思是:Cancel比Try接口先执行
- 对于已经空回滚的业务,按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,此时的Try接口不应该执行,但之前被阻塞的Try操作恢复,继续执行Try,就永远不可能Confirm或Cancel,事务一直处于中间状态,这就是业务悬挂。
- 执行Try操作时,应当判断Cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的Try操作,避免悬挂 => 防悬挂控制
- 思路:在Cancel空回滚成功之前先记录该条事务的全局事务ID - XID或业务主键,标识这条记录已经回滚过,Try接口先检查这条事务的XID或业务主键,如果已经标记为回滚成功过,则不执行Try的业务操作。
- 所谓幂等就是操作一次和操作多次的执行效果是一样的。
- 比如:我们的库存扣减操作,如果某一步操作报错,导致需要回滚重试,结果每次重试都会重复扣减库存,那这样肯定是不对的。
- 所以为了保证TCC二阶段的Confirm、Cancel的提交重试机制不会引发数据的不一致,要求二阶段的Confirm和Cancel接口保证幂等,这样就不会使得我们的资源发生重复消耗,如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
解决思路:在上述"分支事务记录"中增加执行状态,每次执行前都查询该状态,通过该状态字段来判断是否执行过。
Saga模式是Seata提供的长事务解决方案,由蚂蚁金服主要贡献,也分为两个阶段:
如图:T1~T3都是正向的业务流程,都对应着一个冲正逆向操作C1~C3Sage和TCC有些类似,都是补偿型事务,与TCC不同的是,Saga不需要Try,而是直接进行Confirm、Cancel操作。
优势:
- 一阶段直接提交本地事务,无锁,高性能
- 事件驱动架构,不会阻塞和等待,参与者可异步执行,高吞吐
- 补偿服务即正向服务的"反向",易于理解,易于实现
缺点:
- 软状态持续事件不确定,时效性差,属于最终一致性
- 没有锁,不保证隔离性,没有事务隔离,会有脏写
- Saga正向服务与补偿服务也需要业务开发者手动实现,因此也是有业务代码侵入的
我们从以下几个方面来对比四种实现:
一致性:能否保证事务的一致性?强一致还是最终一致?
隔离性:事务之间的隔离性如何?
代码侵入:是否需要对业务代码改造?
性能:有无性能损耗?
场景:常见的业务场景
Seata的TC服务作为分布式事务核心,一定要保证集群的高可用性和异地容灾。
但集群并不能确保100%安全,万一集群所在机房故障怎么办?