SpringCloud Alibaba系列——17Seata AT模式源码分析(上)
作者 | 一起撸Java
来源 |今日头条
学习目标
- Seata AT模式源码流程
第1章 AT模式流程
1.1 思维流程推导
上文中已经讲了AT模式的大体原理,在源码中,通过README也能看出来AT模式的使用,那本文将从底层源码层面去分析AT模式的原理,在分析原理之前咱们先来看三幅图,理解一下他的工作思路和模式:
先看看思维推导图1.2 初始化流程推导
1.3 执行流程推导
第2章 源码分析
2.1 SeataAutoConfiguration
对于seata源码的研究主要看seata如何拦截业务SQL生成undo_log数据,如何在一阶段完成后提交全局事务,如何在一阶段业务失败后通过undo_log回滚事务,进行事务补偿。
seata也是与spring整合使用的,结合SpringBoot,seata也是做了一些自动配置seata的自动配置类命名非常的直接,就叫做:SeataAutoConfiguration,我们打开这个类
@ComponentScan(basePackages = "io.seata.spring.boot.autoconfigure.properties")@ConditionalOnProperty(prefix = StarterConstants.SEATA_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)@Configuration@EnableConfigurationProperties({SeataProperties.class})public class SeataAutoConfiguration { }
首先,@ComponentScan扫描了一下properties包,加载了一大堆类似SeataProperties的Bean对象。
@ConditionalOnProperty将配置类生效条件设置为seata.enabled=true,默认值是true,所以可以开关分布式事务功能(在client端的file.conf里面可以配置)。
@Configuration表明,SeataAutoConfiguration被定义为了spring的配置类。
@EnableConfigurationProperties将配置包转成了一个SeataProperties的Bean对象来使用。
接下来阅读SeataAutoConfiguration的内部代码
@Bean@DependsOn({BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER, BEAN_NAME_FAILURE_HANDLER})@ConditionalOnMissingBean(GlobalTransactionScanner.class)public GlobalTransactionScanner globalTransactionScanner(SeataProperties seataProperties, FailureHandler failureHandler) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Automatically configure Seata"); } return new GlobalTransactionScanner(seataProperties.getApplicationId(), seataProperties.getTxServiceGroup(), failureHandler);}
自动配置的核心点落在了下面的一个Bean,GlobalTransactionScanner。
我们看到构造这个Bean非常的简单,构造方法只需要一个applicationId和txServiceGroup。
applicationId: 就是spring.application.name=你定义的当前应用的名字,例如:userService
txServiceGroup: 就是以applicationId 加上 -seata-service-group命名的,例如:
userService-seata-service-group。如果版本较低的话,那时候可能还不叫seata而是fescar,因此默认命名就是以fescar为后缀。
new了一个GlobalTransactionScanner对象,SeataAutoConfiguration这个自动配置类的作用就结束了。SeataAutoConfiguration只是做了一个启动引导的作用。
2.2 GlobalTransactionScanner
既然核心点落在GlobalTransactionScanner这个类,我们继续关注它。看这个名字其实就可以猜测到一点它的作用,扫描@GlobalTransactional这个注解,并对代理方法进行拦截增强事务的功能。
要了解这个类,不得不先阅读一下它的UML图可以看到,GlobalTransactionScanner主要有4个点值得关注:
1)ApplicationContextAware表示可以拿到spring容器
2)InitializingBean接口,表达了初始化的时候会进行一些操作
3)AbstractAutoProxyCreator表示它会对spring容器中的Bean进行切面增强,也就是我们上面的拦截事务增强的猜测。
4)Disposable接口,表达了spring容器销毁的时候会进行一些操作
这里我们稍微关注一下这4个的执行顺序:
ApplicationContextAware -> InitializingBean -> AbstractAutoProxyCreator -> DisposableBean
2.3 InitializingBean
@Overridepublic void afterPropertiesSet() { if (disableGlobalTransaction) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Global transaction is disabled."); } return; } initClient();}
初始化Seata的Client端的东西,Client端主要包括TransactionManager和ResourceManager。或许是为了简化吧,并没有把initClient这件事从GlobalTransactionScanner里面独立出来一个类。
跟进initClient方法
private void initClient() { //init TM TMClient.init(applicationId, txServiceGroup); //init RM RMClient.init(applicationId, txServiceGroup); registerSpringShutdownHook();}
initClient逻辑并不复杂,单纯调用TMClient.init初始化TransactionManager的RPC客户端,RMClient.init初始化ResourceManager的RPC客户端。seata的RPC采用netty来实现,seata封装简化了一下使用。并注册了一个Spring的ShutdownHook钩子函数
2.3.1 TMClient初始化
@Overridepublic void init() { timerExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { clientChannelManager.reconnect(getTransactionServiceGroup()); } }, SCHEDULE_DELAY_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.MILLISECONDS);...}
启动了一个定时器不断进行重连操作,调用
clientChannelManager.reconnect方法进行重连
void reconnect(String transactionServiceGroup) { List<String> availList = null; try { availList = getAvailServerList(transactionServiceGroup); } catch (Exception e) { ... } ... for (String serverAddress : availList) { try { acquireChannel(serverAddress); } catch (Exception e) { ... } }}
根据transactionServiceGroup获取seata-server的ip地址列表,然后进行重连
private List<String> getAvailServerList(String transactionServiceGroup) throws Exception { List<InetSocketAddress> availInetSocketAddressList = RegistryFactory.getInstance() .lookup(transactionServiceGroup); if (CollectionUtils.isEmpty(availInetSocketAddressList)) { return Collections.emptyList(); } return availInetSocketAddressList.stream() .map(NetUtil::toStringAddress) .collect(Collectors.toList());}
RegistryFactory.getInstance().lookup(transactionServiceGroup);是对不同注册中心做了适配的,默认看下Nacos形式的实现先根据事务分组找到分组所属的server集群名称,这里是default,然后根据集群名称找到server对应ip端口地址
@Overridepublic List<InetSocketAddress> lookup(String key) throws Exception { //default String clusterName = getServiceGroup(key); if (clusterName == null) { return null; } if (!LISTENER_SERVICE_MAP.containsKey(clusterName)) { synchronized (LOCK_OBJ) { if (!LISTENER_SERVICE_MAP.containsKey(clusterName)) { List<String> clusters = new ArrayList<>(); clusters.add(clusterName); List<Instance> firstAllInstances = getNamingInstance().getAllInstances(getServiceName(), getServiceGroup(), clusters); if (null != firstAllInstances) { List<InetSocketAddress> newAddressList = firstAllInstances.stream() .filter(instance -> instance.isEnabled() && instance.isHealthy()) .map(instance -> new InetSocketAddress(instance.getIp(), instance.getPort())) .collect(Collectors.toList()); CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); } subscribe(clusterName, event -> { List<Instance> instances = ((NamingEvent) event).getInstances(); if (null == instances && null != CLUSTER_ADDRESS_MAP.get(clusterName)) { CLUSTER_ADDRESS_MAP.remove(clusterName); } else if (!CollectionUtils.isEmpty(instances)) { List<InetSocketAddress> newAddressList = instances.stream() .filter(instance -> instance.isEnabled() && instance.isHealthy()) .map(instance -> new InetSocketAddress(instance.getIp(), instance.getPort())) .collect(Collectors.toList()); CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); } }); } } } return CLUSTER_ADDRESS_MAP.get(clusterName);}
Seata-server的IP地址已获取到,然后调用acquireChannel
Channel acquireChannel(String serverAddress) { Channel channelToServer = channels.get(serverAddress); if (channelToServer != null) { channelToServer = getExistAliveChannel(channelToServer, serverAddress); if (channelToServer != null) { return channelToServer; } }... channelLocks.putIfAbsent(serverAddress, new Object()); synchronized (channelLocks.get(serverAddress)) { return doConnect(serverAddress); }}
最后将获取到的seata-server的IP地址放到Netty中封装,TmClient就初始化完毕
TmClient初始化总结:
- 启动定时器,尝试进行一次重连seata-server
- 重连时,先从nacos(或则其他配置)中根据分组名称(service_group)找到集群名称(cluster_name)
- 再根据集群名称找到集群ip端口列表
- 从ip列表中选择一个用netty进行连接
2.3.2 RMClient初始化
public static void init(String applicationId, String transactionServiceGroup) { // 获取单例对象 RmRpcClient rmRpcClient = RmRpcClient.getInstance(applicationId, transactionServiceGroup); // 设置ResourceManager的单例对象 rmRpcClient.setResourceManager(DefaultResourceManager.get()); // 添加监听器,监听Server端的消息推送 rmRpcClient.setClientMessageListener(new RmMessageListener(DefaultRMHandler.get())); // 初始化RPC rmRpcClient.init();}
和TMClient想比,RMClient多出了一个监听Server端消息并处理的机制。也就是说TM的职责更多的是主动与Server端通信,比如:全局事务的begin、commit、rollback等。
而RM除了主动操作本地资源外,还会因为全局事务的commit、rollback等的消息推送,从而对本地资源进行相关操作。
设置资源管理器resourceManager,设置消息回调监听器用于接收TC在二阶段发出的提交或者回滚请求,Seata中对ResourceManager,AbstractRMHandler做了SPI适配,以ResouceManager为例:
public class DefaultResourceManager implements ResourceManager { protected void initResourceManagers() { //init all resource managers List<ResourceManager> allResourceManagers = EnhancedServiceLoader.loadAll(ResourceManager.class); if (CollectionUtils.isNotEmpty(allResourceManagers)) { for (ResourceManager rm : allResourceManagers) { resourceManagers.put(rm.getBranchType(), rm); } } }}
可以看到初始化DefaultResouceManager时会使用ClassLoader去加载对应Jar下的实现,而默认AT模式使用的实现是数据库,也就是rm-datasource包下的实现,找实现类路径需要定位到/resources/META-INF/扩展接口全路径去找,就会找到对应的实现类 ResourceManager对应实现类全路径
io.seata.rm.datasource.DataSourceManager,该类中指定了了提交和回滚的方法,DefaultRMHandler对应实现类全路径io.seata.rm.RMHandlerAT,是个接收server消息并做对应提交或者回滚操作的回调处理类。
RMClinet的init()方法与TMClient基本一致
2.3.3 总结
- Spring启动时,初始化了2个客户端TmClient、RmClient
- TmClient与seata-server通过Netty建立连接并发送消息
- RmClient与seata-server通过Netty建立连接,负责接收二阶段提交、回滚消息并在回调器(RmHandler)中做处理