美文网首页
spring AbstractRoutingDataSource

spring AbstractRoutingDataSource

作者: 伊丽莎白菜 | 来源:发表于2021-12-31 19:55 被阅读0次

〇、问题描述

有这么一个项目,底层框架(无源码jar引用)依赖oracle数据库,而业务代码需要查询另一sqlserver数据源。

为了使这个臃肿的项目跑起来,我们引入了spring的动态数据源AbstractRoutingDataSource,将默认数据源路由设为oracle,供底层代码使用。而业务代码根据需要,可以将当前线程的数据源切换为sqlserver。

这个方案稳定运行了将近一年,但随着项目愈加臃肿,环境愈加复杂,有一天它突然不好使了...

我被委托排查这个问题。

既然项目本身就不怎么优雅,我也不寻求优雅的解决方案,能跑就行。

一、现状分析

1.1 依赖版本

没错,mybatis和hibernate都在跑( ̄ー ̄)

组件 版本
springboot 2.2.4.RELEASE
spring framework 5.2.1.RELEASE
mybatis-plus 3.3.0
mybatis 3.5.3
hibernate 5.4.10.Final

1.2 数据源实现

DataSourceAOP.png

动态数据源的实现结构如图,组件释义如下,代码中隐去了具有业务含义的对象命名:

  1. DynamicDataSource继承自org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,使用HashMap<DataSourceKey, javax.sql.DataSource>存储数据源对象;
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Map<Object, Object> datasources;

    public DynamicDataSource() {
        datasources = new HashMap<>();
        super.setTargetDataSources(datasources);
    }

    public <T extends DataSource> void addDataSource(DataSourceKey key, T data) {
        datasources.put(key, data);
    }

    @Override
    public Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSourceKey();
    }

    @Override
    public DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }
}
  1. DataSourceKey是一个枚举,定义了数据源的路由键;

    public enum DataSourceKey {
        DS1, DS2
    }
    
  2. DataSourceHolder使用ThreadLocal<DataSourceKey>控制当前线程的数据源路由切换;

    public class DataSourceHolder {
    
        private static final ThreadLocal<DataSourceKey> dataSourceKey = new ThreadLocal<>();
    
        public static DataSourceKey getDataSourceKey() {
            return dataSourceKey.get();
        }
        
        public static void setDataSourceKey(DataSourceKey type) {
            dataSourceKey.set(type);
        }
    
        public static void clearDataSourceKey() {
            dataSourceKey.remove();
        }
    }
    
  3. 通过aop切方法注解,实现数据源的自动切换。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DataSourceKey value() default DataSourceKey.DS1;

}

---
@Slf4j
@Aspect
@Order(-1)
public class DataSourceAOP {

    @Before("@annotation(ds)")
    public void changeDataSource(JoinPoint point, DataSource ds) {
        try {
            DataSourceKey dataSourceKey = ds.value();
            DataSourceHolder.setDataSourceKey(dataSourceKey);
        } catch (Exception e) {
            log.error("数据源[{}]切换异常,目标方法:[{}]", ds.value(), point.getSignature(), e);
        }
    }

    @After("@annotation(ds)")
    public void restoreDataSource(DataSource ds) {
        DataSourceHolder.clearDataSourceKey();
    }
}

二、 尝试修复

2.1 尝试修正dataSource

一脸蒙圈地开始查问题,我意识到mybatis会话管理器SqlSessionFactory持有错误的dataSource对象,表面现象是这样,原因不详...

既然它错了,那我就强行修正呗。这属性还不对外暴露,我只能选择反射。像这样:

        Environment environment = sessionFactory.getConfiguration().getEnvironment();
        Field dataSourceField = environment.getClass().getDeclaredField("dataSource");
        dataSourceField.setAccessible(true);
        dataSourceField.set(environment, dynamicDataSource.determineTargetDataSource());

重启,测试,没报错!完事交差,回工位喝茶了。

然而,几分钟后,同事就告诉我,sqlserver是好了,但底层的oracle开始报错了。

2.2 尝试动态代理

上面我犯了一个低级错误,sessionFactory对象是全局单例的,我把它当单线程程序处理了,随便测两下肯定一堆错误。

思来想去,简单暴力的方式就是给sessionFactory做一个动态代理,在获取连接配置对象时,给每一个线程拷贝一份单独的副本。像这样:

        Configuration configuration = sessionFactory.getConfiguration();
        final Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(configuration.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Object result =  methodProxy.invokeSuper(o, objects);
                if ("getEnvironment".equals(method.getName())) {
                    Environment environment = (Environment) result;
                    Environment environmentCopy = new Environment.Builder(environment.getId() + "." + Thread.currentThread().getId())
                            .dataSource(dynamicDataSource.determineTargetDataSource()).transactionFactory(environment.getTransactionFactory()).build();
                    return environmentCopy;
                }
                return result;
            }
        });
        Configuration configurationProxy = (Configuration) enhancer.create();
        BeanUtil.copyProperties(configuration, configurationProxy);
        ReflectUtil.setFieldValue(sessionFactory, "configuration", configurationProxy);

实际我写的代码更多,但没卵用,因为代理后根本用不了,直接抛错...

算了,我告诉同事搞不定,他去别的服务里写了,反正暴露接口调用就能实现了︿( ̄︶ ̄)︿

三、仔细研究

回去睡了一觉,第二天仍然打开这段代码,看来简单粗暴的方法解决不了啊。

3.1 问题定位

经过仔细研究,我定位到org.springframework.transaction.support.TransactionSynchronizationManager中也有几个ThreadLocal,缓存了一些数据库连接和事务资源。

我切我的,他缓存他的,能好使才怪!

3.2 打补丁

优雅的解决方案我已经放弃,我决定直接覆盖同名类,能跑就行。

经过反复调试,我得出了以下解决方案:

  1. 动态数据源加两个方法

    目的是获取默认数据源;

     private Object defaultTargetDataSource;
    
        @Override
        public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
            super.setDefaultTargetDataSource(defaultTargetDataSource);
            this.defaultTargetDataSource = defaultTargetDataSource;
        }
    
        public Object getDefaultTargetDataSource() {
            return defaultTargetDataSource;
        }
    
  2. TransactionSynchronizationManager打补丁

    • 当发生数据源切换,备份默认数据源相关的线程局部变量,然后清空ThreadLocal,这样新数据源的相关资源可以重新初始化;
    • 当再次切换回默认数据源,把备份的资源还原回来;
    • 不备份不好使,默认数据源的线程局部变量没法重新生成,不知道为啥,不管它了,能跑就行。
    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    private static final ThreadLocal<Map<Object, Object>> resourcesBak =
            new NamedThreadLocal<>("Transactional resources bak");

    public static void resolveResource() {
        Map<Object, Object> resource = resources.get();
        if (resource == null) {
            return;
        }
        for (Object o : resource.keySet()) {
            if (!(o instanceof DynamicDataSource)) {
                continue;
            }
            DynamicDataSource dynamicDataSource = (DynamicDataSource) o;
            if (dynamicDataSource.determineTargetDataSource() != dynamicDataSource.getDefaultTargetDataSource()) {
                resourcesBak.set(resource);
                resources.remove();
                break;
            }
            Map<Object, Object> resourceBak = resourcesBak.get();
            if (resourceBak == null) {
                break;
            }
            ConnectionHolder connectionHolder = (ConnectionHolder) resourceBak.get(dynamicDataSource);
            try {
                if (!connectionHolder.getConnection().isClosed()) {
                    resources.set(resourceBak);
                } else {
                    resourcesBak.remove();
                }
            } catch (SQLException e) {
                logger.error("connection error, connectionHolder: [{" + connectionHolder + "}]", e);
            }
        }
    }
  1. DataSourceHolder里调用以上方法
    public static void setDataSourceKey(DataSourceKey type) {
        dataSourceKey.set(type);
        TransactionSynchronizationManager.resolveResource();
    }

3.3 补丁的补丁

本来清空TransactionSynchronizationManager资源是spring的某些后置任务做的,我提前把它做了,后面会报错。

  • org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor

    修改afterCompletion方法,资源为null不抛异常:

        @Override
        public void afterCompletion(WebRequest request, @Nullable Exception ex) 
            throws DataAccessException {
            if (!decrementParticipateCount(request)) {
                EntityManagerHolder emHolder = (EntityManagerHolder)
           TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
                logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
                // 这里加个非空判断
                if (emHolder != null) {
                   EntityManagerFactoryUtils.closeEntityManager(
                     emHolder.getEntityManager());
                }
            }
        }
    
  • org.springframework.transaction.support.TransactionSynchronizationManager

    修改unbindResource方法,资源为null不抛异常:

        public static Object unbindResource(Object key) {
            Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    //        Object value = doUnbindResource(actualKey);
    //        if (value == null) {
    //            throw new IllegalStateException(
    //                    "No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
    //        }
            return doUnbindResource(actualKey);
        }
    

3.4 顺便做个优化

测试了几下,应该好使了。但有些代码看着不太顺,比如底层没有提供自动配置。顺手优化下,给配置加个开关:

@Slf4j
@Configuration
@Import(DataSourceAOP.class)
@ConditionalOnProperty(name = {"project-name.datasource.dynamic.enabled"}, matchIfMissing = true)
public class DynamicDatasourceConfig {

    public DynamicDatasourceConfig() {
        log.info("动态数据源切面初始化, DataSourceKeys: {}", ArrayUtil.toString(DataSourceKey.values()));
    }
}

完事, git commit && git push -f,大家元旦快乐!

四、后续思考

表面上看,问题解决了,项目能跑了,但是解决方案未免太复杂了,看起来还不太稳定。

仔细回忆了一下这个项目的提交历史,初期我的确做过多数据源的集成与测试。

后来,经过与平台同事的沟通,再加上我的自信,我删除了一些看起来没啥用的平台依赖。

可能正是这个操作引起了项目环境的变化,动态数据源已经不能正常工作了,但直到最近才有需求要查询另一数据库,所以错误得以隐藏。

如果它再出问题,我就出绝招: 回退

有时间还是重构吧,再也不想打这破补丁了(╬▔皿▔)╯**

相关文章

网友评论

      本文标题:spring AbstractRoutingDataSource

      本文链接:https://www.haomeiwen.com/subject/njcnqrtx.html