关于springboot:SpringBoot多数据源事务解决方案

背景

之前有文章提供了springboot多数据源动静注册切换的整合计划,在后续应用过程中,发现在事务管制中有多种bug产生,决定对此问题进行剖析与解决

前情提要

多数据源切换流程结构图如下所示,蕴含几个组成元素

  • 自定义的数据源配置解决,通过DruidDataSource对象动静注册到零碎中
  • 自定义数据源标识注解与切面
  • 数据源切换时的上下文线程变量持有者
  • 自定义AbstractRoutingDataSource,实现数据源路由切换

问题剖析

在Controller退出@Transitional注解后,数据源切换会生效,只会操作主库,查问材料后解决方案是将切面的Order设置为-1使之执行程序在事务管制拦挡之前,批改后证实无效,然而后续再次切换别的库或者进行主库操作有效,拿到的connection始终是第一次切换后的库对应的连贯

剖析代码后发现AbstractRoutingDataSource只负责提供getConnection这一层级,然而后续对connection的操作无奈跟踪,我的项目框架mybatis和jdbcTemplate混合应用,后续操作在spring层面对于事务/数据源/连贯这三者的逻辑层面操作是雷同的,jdbcTemplate代码较为简单,所以以此为切入点进一步剖析

通过断点调试会发现sql语句的执行最终会落到execute办法,办法中开始就是通过DataSourceUtils.getConnection获取连贯,这里就是咱们须要追踪的中央,点进去发现跳转到doGetConnection办法,这外面就是咱们须要剖析的具体逻辑

第一行获取的ConnectionHolder就是以后事务对应的线程持有对象,因为咱们晓得,事务的实质就是办法外部的sql执行时对应的是同一个数据库connection,对于不同的嵌套业务办法,惟一雷同的是以后线程ID统一,所以咱们将connection与线程绑定就能够实现事务管制

点进getResource办法,发现dataSource是作为一个key去一个Map汇合里取出对应的contextHolder

到这里咱们如同发现点什么,之前对jdbcTemplatechu实例化设定数据源间接赋值自定义的DynamicDataSource,所以在事物中每次咱们获取connection根据就是DynamicDataSource这个对象作为key,所以每次都会一样了!!

    @Bean
    public JdbcTemplate jdbcTemplate(){
        JdbcTemplate jdbcTemplate = null;
        try{
            jdbcTemplate = new JdbcTemplate(dynamicDataSource());
        }catch (Exception e){
            e.printStackTrace();
        }
        return jdbcTemplate;
    }

后续针对mybatis查找了相干材料,事务管制默认实现是SpringManagedTransaction,源码查看后发现了相熟的DataSourceUtils.getConnection,证实咱们的剖析方向是正确的

解决方案

jdbcTemplate

自定义操作类继承jdbcTemplate重写getDataSource,将咱们获取的DataSource这个对应的key指定到理论切换库的数据源对象上即可

public class DynamicJdbcTemplate extends JdbcTemplate {
    @Override
    public DataSource getDataSource() {
        DynamicDataSource router =  (DynamicDataSource) super.getDataSource();
        DataSource acuallyDataSource = router.getAcuallyDataSource();
        return acuallyDataSource;
    }

    public DynamicJdbcTemplate(DataSource dataSource) {
        super(dataSource);
    }
}
    public DataSource getAcuallyDataSource() {
        Object lookupKey = determineCurrentLookupKey();
        if (null == lookupKey) {
            return this;
        }
        DataSource determineTargetDataSource = this.determineTargetDataSource();
        return determineTargetDataSource == null ? this : determineTargetDataSource;
    }

mybatis

自定义事务操作类,实现Transaction接口,替换TransitionFactory,这里的实现与网上的解决方案略有不同,网上是定义三个变量,datasource(动静数据源对象)/connection(主连贯)/connections(从库连贯),然而框架须要mybatis和jdbctemplate进行对立,mybatis是从connection层面管制,jdbctemplate是从datasource层面管制,所以全副应用键值对存储

public class DynamicTransaction implements Transaction {
    private final DynamicDataSource dynamicDataSource;
    private ConcurrentHashMap<String, DataSource> dataSources;
    private ConcurrentHashMap<String, Connection> connections;
    private ConcurrentHashMap<String, Boolean> autoCommits;
    private ConcurrentHashMap<String, Boolean> isConnectionTransactionals;

    public DynamicTransaction(DataSource dataSource) {
        this.dynamicDataSource = (DynamicDataSource) dataSource;
        dataSources = new ConcurrentHashMap<>();
        connections = new ConcurrentHashMap<>();
        autoCommits = new ConcurrentHashMap<>();
        isConnectionTransactionals = new ConcurrentHashMap<>();
    }

    public Connection getConnection() throws SQLException {
        String dataBaseID = DBContextHolder.getDataSource();
        if (!dataSources.containsKey(dataBaseID)) {
            DataSource dataSource = dynamicDataSource.getAcuallyDataSource();
            dataSources.put(dataBaseID, dataSource);
        }
        if (!connections.containsKey(dataBaseID)) {
            Connection connection = DataSourceUtils.getConnection(dataSources.get(dataBaseID));
            connections.put(dataBaseID, connection);
        }
        if (!autoCommits.containsKey(dataBaseID)) {
            boolean autoCommit = connections.get(dataBaseID).getAutoCommit();
            autoCommits.put(dataBaseID, autoCommit);
        }
        if (!isConnectionTransactionals.containsKey(dataBaseID)) {
            boolean isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connections.get(dataBaseID), dataSources.get(dataBaseID));
            isConnectionTransactionals.put(dataBaseID, isConnectionTransactional);
        }
        return connections.get(dataBaseID);
    }


    public void commit() throws SQLException {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
            boolean autoCommit = autoCommits.get(dataBaseID);
            if (connection != null && !isConnectionTransactional && !autoCommit) {
                connection.commit();
            }
        }
    }

    public void rollback() throws SQLException {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
            boolean autoCommit = autoCommits.get(dataBaseID);
            if (connection != null && !isConnectionTransactional && !autoCommit) {
                connection.rollback();
            }
        }
    }

    public void close() {
        for (String dataBaseID : connections.keySet()) {
            Connection connection = connections.get(dataBaseID);
            DataSource dataSource = dataSources.get(dataBaseID);
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    }

    public Integer getTimeout() {
        return null;
    }
}
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new DynamicTransaction(dataSource);
    }
}
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        //SpringBootExecutableJarVFS.addImplClass(SpringBootVFS.class);
        final PackagesSqlSessionFactoryBean sessionFactory = new PackagesSqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource());
        sessionFactory.setTransactionFactory(new DynamicTransactionFactory());
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mybatis/**/*Mapper.xml"));
        //敞开驼峰转换,避免带下划线的字段无奈映射
        sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(false);
        return sessionFactory.getObject();
    }

事务管理器

事务中库动静切换的问题解决了,然而只针对了主库事务,如果从库操作也须要事务的个性该如何操作呢,这里就须要在注册数据源时针对每个数据源手动注册一个事务管理器

主库是固定的,能够间接在配置Bean中申明masterTransitionManage并设置为默认

    @Bean("masterTransactionManager")
    @Primary
    public DataSourceTransactionManager MasterTransactionManager() {
        return new DataSourceTransactionManager(masterDataSource());
    }

从库的事务管理器咱们能够拿到dataSource初始化对象,而后向Spring容器注册单例对象

 public static void registerSingletonBean(String beanName, Object singletonObject) {
        //将applicationContext转换为ConfigurableApplicationContext
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context;
        //获取BeanFactory
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
        if(configurableApplicationContext.containsBean(beanName)) {
            defaultListableBeanFactory.destroySingleton(beanName);
        }
        //动静注册bean.
        defaultListableBeanFactory.registerSingleton(beanName, singletonObject);

    }
 SpringBootBeanUtil.registerSingletonBean(key + "TransactionManager", new DataSourceTransactionManager(druidDataSource));

在应用时只有对@Transitional注解指定transitionFactory名字即可

总结

解决这个问题破费了三天的工夫,查了很多材料和解决方案,很多都是只有参考性或者特异性的,所以还是需把握问题的外围加上局部源码的追踪,比方本文中须要清晰的意识到Transition-Connection-LocalThread三者的关联关系,能力找对排查的方向

后续实现了集成基于JMS(atomikos)的XA两段式提交的全局事务,应用DruidXADataSrouce呈现了druid和atomikos两者线程池交互呈现泄露的状况放弃了,给小伙伴们避个坑

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理