springboot 同一方法内,多数据源切换,包含事务

最近项目遇到了同一方法内,主数据库操作数据后,需往其他数据源同步数据的情景,在此记录一下实现过程,也参照了下其他大牛的代码

主要有两种实现方式

  1. 通过主动方式切换数据源
  2. 直接获取JdbcTemplate

参考文章:
SpringBoot多数据源切换详解,以及开启事务后数据源切换失败处理
springboot+mybatis解决多数据源切换事务控制不生效的问题

实现流程

  • 一、禁用数据库自动配置
  • 二、主动切换数据源方式
    • 2.1 配置数据源类型
    • 2.2 配置数据源切换上下文
    • 2.3 配置动态切换数据源类
    • 2.4 重写Transaction
    • 2.5 使用AOP或Interceptor配置默认数据源
  • 三、配置数据源
  • 四、service方法内切换数据源
    • 4.1 主动切换方式
    • 4.2 JdbcTemplate方式
  • 五、其他想法

一、禁用数据库自动配置

禁用数据库自动配置需在Application类上增加配置,可在@SpringBootApplication注解后,也可在@EnableAutoConfiguration注解后配置。

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

有时也需要屏蔽如下类:

DataSourceTransactionManagerAutoConfiguration.class
JdbcTemplateAutoConfiguration.class
HibernateJpaAutoConfiguration.class

二、主动切换数据源方式

2.1 配置数据源类型

通常采用常量或者枚举类型

public enum  DBType {
    
    one("one"),

    two("two");

    private String value;

    DBType(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

2.2 配置数据源切换上下文

public class DBContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal<>();
    /**
     * 设置数据源
     * @param DBType
     */
    public static void setDbType(DBType dbType) {
        contextHolder.set(dbType.getValue());
    }

    /**
     * 取得当前数据源
     * @return
     */
    public static String getDbType() {
        return (String) contextHolder.get();
    }

    /**
     * 清除上下文数据
     */
    public static void clearDbType() {
        contextHolder.remove();
    }
}

2.3 配置动态切换数据源类

需要继承AbstractRoutingDataSource类,并重写determineCurrentLookupKey()方法,从数据源类型中获取当前线程的数据源类型。

public class DynamicDataSource extends AbstractRoutingDataSource  {

    @Override
    protected Object determineCurrentLookupKey() {
        return  DBContextHolder.getDbType();
    }
}

2.4 重写Transaction

当我们配置了事物管理器和拦截Service中的方法后,每次执行Service中方法前会开启一个事务,并且同时会缓存一些东西:DataSource、SqlSessionFactory、Connection等,所以,我们在外面再怎么设置要求切换数据源也没用,因为Conneciton都是从缓存中拿的,所以我们要想能够顺利的切换数据源,实际就是能够动态的根据DatabaseType获取不同的Connection,并且要求不能影响整个事物的特性。

主要包含两个类:

import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import org.apache.ibatis.transaction.Transaction;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.apache.commons.lang3.Validate.notNull;

/**
* 

多数据源切换,支持事务

* * @author 高仕立 * @date 2018/2/6 9:09 * @since */
public class MultiDataSourceTransaction implements Transaction{ private static final Log LOGGER = LogFactory.getLog(MultiDataSourceTransaction.class); private final DataSource dataSource; private Connection mainConnection; private String mainDatabaseIdentification; private ConcurrentMap<String, Connection> otherConnectionMap; private boolean isConnectionTransactional; private boolean autoCommit; public MultiDataSourceTransaction(DataSource dataSource) { notNull(dataSource, "No DataSource specified"); this.dataSource = dataSource; otherConnectionMap = new ConcurrentHashMap<>(); mainDatabaseIdentification=DBContextHolder.getDbType(); } /** * {@inheritDoc} */ @Override public Connection getConnection() throws SQLException { String databaseIdentification = DBContextHolder.getDbType(); if (databaseIdentification.equals(mainDatabaseIdentification)) { if (mainConnection != null) return mainConnection; else { openMainConnection(); mainDatabaseIdentification =databaseIdentification; return mainConnection; } } else { if (!otherConnectionMap.containsKey(databaseIdentification)) { try { Connection conn = dataSource.getConnection(); otherConnectionMap.put(databaseIdentification, conn); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } } return otherConnectionMap.get(databaseIdentification); } } private void openMainConnection() throws SQLException { this.mainConnection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.mainConnection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.mainConnection, this.dataSource); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "JDBC Connection [" + this.mainConnection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } } /** * {@inheritDoc} */ @Override public void commit() throws SQLException { if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Committing JDBC Connection [" + this.mainConnection + "]"); } this.mainConnection.commit(); for (Connection connection : otherConnectionMap.values()) { connection.commit(); } } } /** * {@inheritDoc} */ @Override public void rollback() throws SQLException { if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Rolling back JDBC Connection [" + this.mainConnection + "]"); } this.mainConnection.rollback(); for (Connection connection : otherConnectionMap.values()) { connection.rollback(); } } } /** * {@inheritDoc} */ @Override public void close() throws SQLException { DataSourceUtils.releaseConnection(this.mainConnection, this.dataSource); for (Connection connection : otherConnectionMap.values()) { DataSourceUtils.releaseConnection(connection, this.dataSource); } } @Override public Integer getTimeout() throws SQLException { return null; } }
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;

import javax.sql.DataSource;

/**
 * 

支持Service内多数据源切换的Factory

* * @author 高仕立 * @date 2018/2/6 9:18 * @since */
public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { DBContextHolder.setDbType(DBType.one); return new MultiDataSourceTransaction(dataSource); } }

2.5 使用AOP或Interceptor配置默认数据源

采用拦截器举例:

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class DBInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    	// 设置默认数据源
        DBContextHolder.setDbType(DBType.one);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
		// 清除数据源
        DBContextHolder.clearDbType();
    }
}

三、配置数据源

根据配置文件决定使用哪个数据源配置生效

@EnableTransactionManagement
@ConditionalOnProperty(value  = "run.datasource.config", havingValue = "false")
@Configuration
public class DataSourceConfig {

    @Resource
    private Environment env;

    //厂商平台配置数据库
    @Bean(name = "one")
    public DataSource one() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        ds.setUniqueResourceName("one");
        ds.setPoolSize(5);
        ds.setXaProperties(build("spring.datasource.druid.one."));
        return ds;
    }

    @Bean(name = "two")
    public DataSource two() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        ds.setUniqueResourceName("two");
        ds.setPoolSize(5);
        ds.setXaProperties(build("spring.datasource.druid.two."));
        return ds;
    }

    /**
     * 动态数据源配置
     * @return
     */
    @Bean
    @Primary
    public DataSource multipleDataSource(@Qualifier("one") DataSource one,
                                         @Qualifier("two") DataSource two) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBType.one.getValue(), one);
        targetDataSources.put(DBType.two.getValue(), two);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(one);
        return dynamicDataSource;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(multipleDataSource(one(),two()));
        // 使用自定义的多数据源事务工厂,如采用JdbcTemplate方式可不配置
        sqlSessionFactory.setTransactionFactory(new MultiDataSourceTransactionFactory());
        //添加XML目录
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactory.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
        sqlSessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
        return sqlSessionFactory.getObject();
    }

	// 此处是初始化JdbcTemplate,可直接获取到数据源连接
    @Bean(name = "jdbc_two")
    public JdbcTemplate secondJdbcTemplate(
            @Qualifier("two") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    private Properties build(String prefix) {
        Properties prop = new Properties();

        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));

        prop.put("initialSize", env.getProperty("spring.datasource.druid.initialSize", Integer.class));
        prop.put("minIdle", env.getProperty("spring.datasource.druid.minIdle", Integer.class));
        prop.put("maxActive", env.getProperty("spring.datasource.druid.maxActive", Integer.class));
        prop.put("maxWait", env.getProperty("spring.datasource.druid.maxWait", Integer.class));
        prop.put("timeBetweenEvictionRunsMillis", env.getProperty("spring.datasource.druid.timeBetweenEvictionRunsMillis", Integer.class));
        prop.put("minEvictableIdleTimeMillis", env.getProperty("spring.datasource.druid.minEvictableIdleTimeMillis", Integer.class));
        prop.put("validationQuery", env.getProperty("spring.datasource.druid.validationQuery"));
        prop.put("testWhileIdle", env.getProperty("spring.datasource.druid.testWhileIdle", Boolean.class));
        prop.put("testOnBorrow", env.getProperty("spring.datasource.druid.testOnBorrow", Boolean.class));
        prop.put("testOnReturn", env.getProperty("spring.datasource.druid.testOnReturn", Boolean.class));
        prop.put("filters", env.getProperty("spring.datasource.druid.filters"));
        return prop;

    }
}

四、service方法内切换数据源

方法上需要配置事务@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

4.1 主动切换方式

根据配置文件,获取所有待操作的数据源,然后调用DBContextHolder的切换数据源方法,由于其余数据库的表结构都是一致的,所以调用同一方法操作各个数据源。

List<DBType> dbs = DbUtils.getAllDBType();
for (DBType db : dbs){
     DBContextHolder.setDbType(db);
     mapper.saveAnother(entity);
 }

4.2 JdbcTemplate方式

直接获取初始化的JdbcTemplate集合,逐个进行操作。

List<JdbcTemplate>  jdbcTemplateList = JdbcTemplateUtils.getJdbcTemplates();
SqlContext sqlContext = SQLTemplate.createSql(entity);
for (JdbcTemplate jdbc : jdbcTemplateList){
  jdbc.update(sqlContext.getSql(), sqlContext.getParams());
}

其中的JdbcTemplate可以通过如下方式获取:

SpringContextHolder.getBean("jdbc_two");

五、其他想法

一般情况下,不要直接操作跨项目的数据库,最好让其他项目(暂且叫做客户端)暴露接口,通过远程调用的方式通知其他客户端有数据变动,然后各个项目根据数据变动进行相应的操作,并返回相应的操作结果给服务端。同时服务端提供相应的查询结果,供客户端定时进行数据的对比,防止有遗漏的数据变动。

你可能感兴趣的:(springboot实战,mybatis)