Seata源码(1)分布式事务中全局事务如何开启(AT模式)

326 阅读5分钟

整体流程

在了解Seata整体运行流程时,需要梳理一下AT模式下分布式事务直接整个数据是怎么流转的。可以参照官网上的两段提交,大致流程如下:

Seata事务使用的是全局事务注解作为开始,在service上面添加@GlobalTransactional注解即可达到开启分布式事务的目的。

代码分析

启动类

很显然是通过AOP切面代理,识别注解。进行织入,填充对应的业务逻辑。我们可以从starter入手,启动Jar包是io.seata:seata-spring-boot-starter:1.6.0。再找到spring.factories文件。

很容易找到SeataAutoConfiguration就是我们要找的配置类。配置类主要功能就是构造了一个scanner。

找到wrapIfNecessary方法,可以看到具体的拦截和切面织入的操作。

if (!existsAnnotation(new Class[]{serviceInterface})
    && !existsAnnotation(interfacesIfJdk)) {
    return bean;
}

判断类和方法上是否有GlobalTransaction和GlobalLock注解。增强类为GlobalTransactionalInterceptor

设置全局事务拦截器,如果没有的话就初始话一个拦截器。

切面织入操作

AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
int pos;
for (Advisor avr : advisor) {
    // Find the position based on the advisor's order, and add to advisors by pos
    pos = findAddSeataAdvisorPosition(advised, avr);
    advised.addAdvisor(pos, avr);
}

其中getAdvicesAndAdvisorsForBean我们就可以知道实际织入的切面类就是上一步定义的全局事务拦截器。

至此,整个织入,加载过程完成。

能力依赖

整个构造都是通过集成AbstractAutoProxyCreator类进行的织入。seata中的很多类都是采用这种方式来进行拦截,写入业务逻辑。

XID生成和传输

xid是贯穿整个分布式事务中的一个重要链条,用来判断是否该线程处于全局事务,是否对提交进行一阶段提交,异常回滚等操作的唯一依据。

我们还是GlobalTransactionalInterceptor为切入点。核心处理逻辑在invoke方法上。

public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
        Class<?> targetClass =
            methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
        Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
        if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) {
            final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
            final GlobalTransactional globalTransactionalAnnotation =
                getAnnotation(method, targetClass, GlobalTransactional.class);
            final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class);
            boolean localDisable = disable || (ATOMIC_DEGRADE_CHECK.get() && degradeNum >= degradeCheckAllowTimes);
            if (!localDisable) {
                if (globalTransactionalAnnotation != null || this.aspectTransactional != null) {
                    AspectTransactional transactional;
                    if (globalTransactionalAnnotation != null) {
                        transactional = new AspectTransactional(globalTransactionalAnnotation.timeoutMills(),
                            globalTransactionalAnnotation.name(), globalTransactionalAnnotation.rollbackFor(),
                            globalTransactionalAnnotation.rollbackForClassName(),
                            globalTransactionalAnnotation.noRollbackFor(),
                            globalTransactionalAnnotation.noRollbackForClassName(),
                            globalTransactionalAnnotation.propagation(),
                            globalTransactionalAnnotation.lockRetryInterval(),
                            globalTransactionalAnnotation.lockRetryTimes(),
                            globalTransactionalAnnotation.lockStrategyMode());
                    } else {
                        transactional = this.aspectTransactional;
                    }
                    return handleGlobalTransaction(methodInvocation, transactional);
                } else if (globalLockAnnotation != null) {
                    return handleGlobalLock(methodInvocation, globalLockAnnotation);
                }
            }
        }
        return methodInvocation.proceed();
    }

大体逻辑我们可以梳理一下,获取注解上的配置,构造织入事务的基本参数对象。核心逻辑在handleGlobalTransaction上。我们在探可以发现核心逻辑其实在execute方法中。所有的核心逻辑其实都封装在这个里面。

到这里其实我们还是没有发现XID的踪迹,其实就在beginTransaction中。通过层层追踪后,可以发现XID是通过本地TM发送netty的网络请求到TC,返回本次事务唯一的全局XID。

但是还是有几个疑问,XID在项目中是存储在哪里的了。很容易看到存储类是ContextCore,准确的说是一个接口。实现类有两个

默认实现是JAVA中比较常用的ThreadLocal,通过与线程绑定的方式来传输XID。另一个使用的netty中自带的FastThreadLocal来进行存储。

存储的问题的解决了,传输有两个疑问,本地传输获取和跨服务传输都是怎么实现的了?本文档以默认AT来模式来作为切入点。

本地传输

TM开启全局事务后,本地的数据库操作(增,删,改)需要同步记录到数据库中undo表。当遇到需要全局回滚时,进行SQL的回滚。

前面我们已经知道在开启分布式全局事务时,会将XID绑定到线程上。当发生数据变更操作时,需要取出对应的XID,记录到数据库中。这个时候我们就需要在插入时,找到是否有从ThreadLocal上获取对应的XID。

通过这个自动配置类,大概知道是在这里进行的处理。找到了SeataAutoDataSourceProxyCreator,知道是代理了DataSource。

判断是否是DataSource,不是的话直接返回。这里使用的包装的方式来进行代码增强,而不是拦截的方式。

AT模式下,返回的DataSource包装类是DataSourceProxy。同时也对获取后的connection进行了包装。

我们进入到ConnectionProxy中找到插入数据的地方。

继续追下去会发现根据DbType找到对应undoLog日志管理类。

会将提交的信息记录到数据库中,在采用原始Connection进行commit操作targetConnection.commit();。保证日志一定可以记录到数据库中在进行提交。我们可以到xid是从ConnectionProxy中的ConnectionContext中进行获取的。

我们继续找一下什么时候xid插入到ConnectionContext中去的。经过全局搜索发现在生成预编译对象时,seata同样也进行了包装。

BaseTransactionalExecutor中可以发现获取了全局xid,并通过bind方法绑定到了connectionContext上。这里我们梳理一下大致的传输流程。

DataSourceProxy -> ConnectionProxy -> preparedStatementProxy -> ExecuteTemplate(模板执行器) -> Executor(具体执行器) -> 设置到ConnectionProxy中 -> commit(方法)。

至此完结。本地传输完成。

微服务传输

我们最常见的是用微服务调用各个服务,这个时候我们同样需要把全局xid传递到其他的微服务中。

starter中正好有一个涉及到http的启动类。

可以看到添加一个拦截器,用于接收网络请求中传输过来的xid。

通过header传输过来,header值为TX_XID。如果有就存入ThreadLocal中,标识目前处于全局事务中。现在接收端有了,发送端是如何实现的了?

seata是在springcloud相关的包里面去定义的这些拦截配置处理。打开spring.factories

可以发现分别拦截了常用的restTemplate和微服务最常见的工具feign。我们来看看是如何进行拦截处理的。这一块逻辑其实不复杂。

1. restTemplate

通过查找所有RestTemplate的实现列表。注入自定义的拦截器

2. feign

替换feign中自带的client处理类,进行包装。

seata自带的spi注入也提供了其他请求方式的xid传输

默认支持阿里内部的几个rpc框架(dubbo,sofa,hsf)。还有一个微博的rpc框架。如果我们有自己的远程调用方式,比如说forest,retrofit等。可以利用spring框架的特性,在框架层面进行拦截,传入全局参数xid即可。