- seata版本:1.4.0,但1.4以下的所有版本也都有这个问题
- 问题描述:在一个全局事务中,一个分支事务上的纯查询操作突然卡住了,没有任何反馈(日志/异常),直到消费端RPC超时
问题排查
- 整个流程在一个全局事务中,消费者和提供者可以看成是全局事务中的两个分支事务,消费者 --> 提供者
- 消费者先执行本地的一些逻辑,然后向提供者发送RPC请求,确定消费者发出了请求已经并且提供者接到了请求
- 提供者先打印一条日志,然后执行一条纯查询SQL,如果SQL正常执行会打印日志,但目前的现象是只打印了执行SQL前的那条日志,而没有打印任何SQL相关的日志。找DBA查SQL日志,发现该SQL没有执行
- 确定了该操作只是全局事务下的一个纯查询操作,在该操作之前,全局事务中的整体流程完全正常
- 其实到这里现象已经很明显了,不过当时想法没转变过来,一直关注那条查询SQL,总在想就算查询超时等原因也应该抛出异常啊,不应该什么都没有。DBA都找不到查询记录,那是不是说明SQL可能根本就没执行啊,而是在执行SQL前就出问题了,比如代理?
- 借助arthas的watch命令,一直没有东西输出。第一条日志的输出代表这个方法一定执行了,迟迟没有结果输出说明当前请求卡住了,为什么卡住了呢?
- 借助arthas的thread命令
thread -b
、thread -n
,就是要找出当前最忙的线程。这个效果很好,有一个线程CPU使用率92%
,并且因为该线程导致其它20多个Dubbo线程BLOCKED
了。堆栈信息如下 - 分析堆栈信息,已经可以很明显的发现和seata相关的接口了,估计和seata的数据源代理有关;同时发现CPU占用最高的那个线程卡在了
ConcurrentHashMap#computeIfAbsent
方法中。难道ConcurrentHashMap#computeIfAbsent
方法有bug? - 到这里,问题的具体原因我们还不知道,但应该和seata的数据源代理有点关系,具体原因我们需要分析业务代码和seata代码
问题分析
ConcurrentHashMap#computeIfAbsent
这个方法确实有可能出问题:如果两个key的hascode相同,并且在对应的mappingFunction中又进行 了computeIfAbsent操作,则会导致死循环,具体分析参考这篇文章:https://juejin.cn/post/6844904191077384200
Seata数据源自动代理
相关内容之前有分析过,我们重点来看看以下几个核心的类:
- SeataDataSourceBeanPostProcessor
- SeataAutoDataSourceProxyAdvice
- DataSourceProxyHolder
SeataDataSourceBeanPostProcessor
SeataDataSourceBeanPostProcessor
是BeanPostProcessor
实现类,在postProcessAfterInitialization
方法(即Bean初始化之后)中,会为业务方配置的数据源创建对应的seata代理数据源
public class SeataDataSourceBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DataSource) {
//When not in the excludes, put and init proxy.
if (!excludes.contains(bean.getClass().getName())) {
//Only put and init proxy, not return proxy.
DataSourceProxyHolder.get().putDataSource((DataSource) bean, dataSourceProxyMode);
}
//If is SeataDataSourceProxy, return the original data source.
if (bean instanceof SeataDataSourceProxy) {
LOGGER.info("Unwrap the bean of the data source," +
" and return the original data source to replace the data source proxy.");
return ((SeataDataSourceProxy) bean).getTargetDataSource();
}
}
return bean;
}
}
SeataAutoDataSourceProxyAdvice
SeataAutoDataSourceProxyAdvice
是一个MethodInterceptor,seata中的SeataAutoDataSourceProxyCreator
会针对DataSource类型的Bean
创建动态代理对象,代理逻辑 就是SeataAutoDataSourceProxyAdvice#invoke
逻辑。即:执行数据源AOP代理对象
的相关方法时候,会经过其invoke
方法,在invoke
方法中再根据当原生数据源,找到对应的seata代理数据源
,最终达到执行seata代理数据源
逻辑的目的
public class SeataAutoDataSourceProxyAdvice implements MethodInterceptor, IntroductionInfo {
......
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (!RootContext.requireGlobalLock() && dataSourceProxyMode != RootContext.getBranchType()) {
return invocation.proceed();
}
Method method = invocation.getMethod();
Object[] args = invocation.getArguments();
Method m = BeanUtils.findDeclaredMethod(dataSourceProxyClazz, method.getName(), method.getParameterTypes());
if (m != null) {
SeataDataSourceProxy dataSourceProxy = DataSourceProxyHolder.get().putDataSource((DataSource) invocation.getThis(), dataSourceProxyMode);
return m.invoke(dataSourceProxy, args);
} else {
return invocation.proceed();
}
}
}
DataSourceProxyHolder
流程上我们已经清楚了,现在还有一个问题,如何维护原生数据源
和seata代理数据源
之间的关系?通过DataSourceProxyHolder
维护,这是一个单例对象,该对象中通过一个ConcurrentHashMap维护两者的关系:原生数据源
为key --> seata代理数据源
为value
public class DataSourceProxyHolder {
public SeataDataSourceProxy putDataSource(DataSource dataSource, BranchType dataSourceProxyMode) {
DataSource originalDataSource = dataSource;
......
return CollectionUtils.computeIfAbsent(this.dataSourceProxyMap, originalDataSource,
BranchType.XA == dataSourceProxyMode ? DataSourceProxyXA::new : DataSourceProxy::new);
}
}
// CollectionUtils.java
public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<? super K, ? extends V> mappingFunction) {
V value = map.get(key);
if (value != null) {
return value;
}
return map.computeIfAbsent(key, mappingFunction);
}
客户端数据源配置
- 配置了两个数据源:
DynamicDataSource
、P6DataSource
P6DataSource
可以看成是对DynamicDataSource
的一层包装- 我们暂时不去管这个配置合不合理,现在只是单纯的基于这个数据源配置分析问题
@Qualifier("dsMaster")
@Bean("dsMaster")
DynamicDataSource dsMaster() {
return new DynamicDataSource(masterDsRoute);
}
@Primary
@Qualifier("p6DataSource")
@Bean("p6DataSource")
P6DataSource p6DataSource(@Qualifier("dsMaster") DataSource dataSource) {
P6DataSource p6DataSource = new P6DataSource(dsMaster());
return p6DataSource;
}
分析过程
假设现在大家都已经知道了 ConcurrentHashMap#computeIfAbsent 可能会产生的问题
,已知现在产生了这个问题,结合堆栈信息,我们可以知道大概哪里产生了这个问题。
1、ConcurrentHashMap#computeIfAbsent
会产生这个问题的前提条件是:两个key的hashcode相同
;mappingFunction中对应了一个put操作
。结合我们seata的使用场景,mappingFunction对应的是DataSourceProxy::new
,说明在DataSourceProxy的构造方法中可能会触发put操作
执行AOP代理数据源相关方法 =>
进入SeataAutoDataSourceProxyAdvice切面逻辑 =>
执行DataSourceProxyHolder#putDataSource方法 =>
执行DataSourceProxy::new =>
AOP代理数据源的getConnection方法 =>
原生数据源的getConnection方法 =>
进入SeataAutoDataSourceProxyAdvice切面逻辑 =>
执行DataSourceProxyHolder#putDataSource方法 =>
执行DataSourceProxy::new =>
DuridDataSource的getConnection方法