前言
分布式项目只要有业务交互就会涉及到分布式事务问题,事务通常分为三步:创建事务、执行事务、提交事务或回滚事务,单机模式下只要一个事务可以依赖数据库的事务实现,而分布式事务往往涉及到多个项目多个数据库的同步更新操作,此时就需要有一套分布式事务解决方案,否则就会出现分布式系统数据不一致的问题。所以分布式事务解决就是要将多个事务当作一个事务来执行,要么全部提交成功要么全部回滚。
一、常用分布式解决方案
1、基于XA的二阶段提交
XA是X/Open组织提出的二阶段提交的分布式事务规范,XA规范核心角色是事务管理器(Transaction Manager) 和 资源管理器 (Resource Manager),资源管理器就是本地的事物,事务管理器负责协调各个资源管理器事务的提交和回滚。
二阶段提交主要将事务拆分成功两个阶段
1、事务准备阶段:事务管理器通知各个资源管理器准备事务,各个资源管理器接受到通知之后在本地执行事务但是不提交,并且写好本地的redo和undo日志,然后将事务执行结果上报给事务管理器
2、事务提交阶段:事务管理器根据各个资源管理器上报的结果决定是提交事务还是回滚事务,然后通知各个资源管理器,各个资源管理器本地提交事务或回滚事务,然后再上报提交或回滚的结果给事务管理器
二阶段提交方案的问题
1、第一阶段基本上不会有什么问题,如果有问题的话直接回滚即可,一般会出问题的可能是第二阶段
2、第二阶段事务管理器下发通知之后,各个资源管理器就开始执行提交或回滚,但是此时还是会出现提交或回滚失败的问题。比如以下场景:
事务管理器通知提交,资源管理器1接收到提交指令提交本地事务;资源管理器2由于宕机没有接收到提交指令而没有提交事务,此时就会出现事务提交不一致的问题了。
3、异常问题处理方案
a.本地资源管理器在执行事务时需要记录redo和undo日志
b.当资源管理器宕机重启时,根据本地redo和undo日志,询问事务管理器当前事务的执行状态,然后继续提交或回滚事务
c.当事务管理器宕机时,如果指令未下发,可以通过记录事务日志的方式根据本地日志进行下发指令;如果指令已经下发,那么各个资源管理器已经提交或回滚事务了,也没有问题
d.第二阶段由于只需要执行提交和回滚命令,而事务中的业务实际已经执行完成,所以提交或回滚命令的执行时间是非常短的,所以出现问题的概率本身就不是很大
4、二阶段提交还有一个问题是整体流程较长,就会导致各个资源管理器占用锁的时间就变长了,必须等待事务提交完成才会释放锁资源,所以高并发情况下不建议采用,否则会导致大量的请求被阻塞在锁竞争上
2、基于TCC的事务补偿方案
事务分成事务执行、事务提交和事务回滚,TCC本质就是实现了事务的三步,包括事务执行(Try)、事务提交(Confirm)、事务回滚(Cancel),TCC核心思想是针对每一个操作都注册一个对应的确认和取消操作。
a.对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
b.实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
3、基于MQ的最终一致性方案
基于MQ的一致性相当于是异步实现的分布式事务,将事务分成了两个不关联的本地事务,基于MQ进行同步,根据最终结果实现一致性从而达到分布式事务。但是最终一致性方案的一致性会有所延迟,并不能保证实时的一致性,另外还需要保证MQ是高可用且不会丢失消息的情况,否则可能会丢失事务的一部分。
二、基于Seata实现的分布式事务解决方案
Seata是阿里开源的分布式事务解决方案,Seata的前身是阿里开源的分布式事务中间件Fescar,后来更名为Seata,意思是Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。
Seata是基于XA的二阶段提交方案演变而来的,核心角色分成了三个:
Transcation Coordinator(TC):事务协调器,负责维护全局事务的状态,以及协调全局事务的提交和回滚
Transcation Manager(TM):控制全局事务的边界,负责开启一个全局事务,并发起最终的全局事务的提交或回滚
Resource Manager(RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一次完整的分布式事务过程如下:
1、TM向TC申请一个全局事务,TC创建一个新的全局事务,并有一个全局事务唯一ID为XID
2、XID在微服务调用链路的上下文中用于传播
3、RM向TC注册分支事务,并由XID对应的全局事务管理
4、TM向TC发起全局事务XID的提交或回滚决议
5、TC调用XID管理的所有分支事务进行提交或回滚
Seata和XA有点类似都是进行了两个阶段,但是有所不同,
1、XA的资源管理器本质是数据库,所以比较依赖数据库的事务;而Seata的资源管理器是第三方包的形式嵌入在应用程序中,更加的灵活
2、XA的事务占有锁资源需要等到第二阶段完成才会释放;而Seata是在第一阶段就将所有的事务进行提交,因为90%以上的请求都会执行成功,所以第二阶段回滚的概率一般不会太高,所以将提交事务的工作放在第一阶段,可以保证虽然全局事务没有结束,但是分支事务可以提前先提交好,如果真的出现异常情况,再通过第二阶段回滚即可,这样就极大的建设了各个分支事务的资源占有时间,提高了整体吞吐量。
三、Seata的实现细节
3.1、Seata的事务如何回滚?
Seata是通过中间件的方式嵌入应用程序的,Seata的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务中提交。这样就可以保证任何提交的业务数据的更新一定有相应的回滚日志存在。基于这样的逻辑Seata可以在第一阶段就将分支事务直接提交,而不用担心后续的回滚。
到了第二阶段,如果全局事务是提交,那么分支事务无需再提交事务,只需要异步将回滚日志删除即可,而这个异步操作的效率比较高;如果全局事务是回滚,那么分支事务就根据XID找到对应的回滚日志进行回滚操作即可。
3.2、分布式事务的传播
全局事务是由于事务协调器发起,实际就是调用链路的第一个发起方,全局事务发起之后会产生全局事务XID,而为了让其他分支事务拿到XID,就需要XID在服务调用链路上进行传播。所以服务调用机制就需要提高一套可以透传XID的机制,比如HTTP请求的header、dubbo的Filter+RpcContext、本地方法的ThreadLocal等
四、Seata使用案例
业务场景:模拟下单业务,需要扣库存,扣用户余额,创建订单三步,假设有订单服务、库存服务、账户服务,那么下单业务就需要调用库存服务和订单服务,而订单服务又需要调用账户服务
案例采用Seata官方提供的Demo
1、maven依赖如下:
<?xml version="1.0" encoding="UTF-8"?> <!-- ~ Copyright 1999-2018 Alibaba Group Holding Ltd. ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>io.seata</groupId> <artifactId>seata-samples</artifactId> <version>1.1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata-samples-dubbo</artifactId> <packaging>jar</packaging> <name>seata-samples-dubbo ${project.version}</name> <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </dependency> <dependency> <groupId>com.101tec</groupId> <artifactId>zkclient</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo-registry-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.spring</groupId> <artifactId>spring-context-support</artifactId> </dependency> <dependency> <groupId>com.alibaba.spring</groupId> <artifactId>spring-context-support</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </dependency> <!--<dependency>--> <!--<groupId>org.apache.dubbo</groupId>--> <!--<artifactId>dubbo-remoting-etcd3</artifactId>--> <!--</dependency>--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build> </project>