最近项目遇到了同一方法内,主数据库操作数据后,需往其他数据源同步数据的情景,在此记录一下实现过程,也参照了下其他大牛的代码
主要有两种实现方式
参考文章:
SpringBoot多数据源切换详解,以及开启事务后数据源切换失败处理
springboot+mybatis解决多数据源切换事务控制不生效的问题
禁用数据库自动配置需在Application类上增加配置,可在@SpringBootApplication
注解后,也可在@EnableAutoConfiguration
注解后配置。
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
或
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
有时也需要屏蔽如下类:
DataSourceTransactionManagerAutoConfiguration.class
JdbcTemplateAutoConfiguration.class
HibernateJpaAutoConfiguration.class
通常采用常量或者枚举类型
public enum DBType {
one("one"),
two("two");
private String value;
DBType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
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();
}
}
需要继承AbstractRoutingDataSource
类,并重写determineCurrentLookupKey()
方法,从数据源类型中获取当前线程的数据源类型。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDbType();
}
}
当我们配置了事物管理器和拦截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);
}
}
采用拦截器举例:
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;
}
}
方法上需要配置事务@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
根据配置文件,获取所有待操作的数据源,然后调用DBContextHolder
的切换数据源方法,由于其余数据库的表结构都是一致的,所以调用同一方法操作各个数据源。
List<DBType> dbs = DbUtils.getAllDBType();
for (DBType db : dbs){
DBContextHolder.setDbType(db);
mapper.saveAnother(entity);
}
直接获取初始化的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");
一般情况下,不要直接操作跨项目的数据库,最好让其他项目(暂且叫做客户端)暴露接口,通过远程调用的方式通知其他客户端有数据变动,然后各个项目根据数据变动进行相应的操作,并返回相应的操作结果给服务端。同时服务端提供相应的查询结果,供客户端定时进行数据的对比,防止有遗漏的数据变动。