SpringBoot實現動態數據源切換及單庫事務控制


引言: 

項目中經常會遇到多數據源的場景,通常的處理是: 操作數據一般都是在DAO層進行處理,使用配置多個DataSource 然后創建多個SessionFactory,在使用Dao層的時候通過不同的SessionFactory進行處理,

但是這樣的操作代碼入侵性比較明顯且配置繁瑣難以維護,,,在這里推薦一個Spring提供的AbstractRoutingDataSource抽象類,它實現了DataSource接口的用於獲取數據庫連接的方法

AbstractRoutingDataSource的內部維護了一個名為 targetDataSources的Map,並提供的setter方法用於設置數據源關鍵字與數據源的關系, 實現類被要求實現其determineCurrentLookupKey()方法,由此方法的返回值決定具體從哪個數據源中獲取連接

一. 注解方式實現動態切換數據源

原理: AbstractRoutingDataSource提供了程序運行時動態切換數據源的方法,在dao類或方法上標注需要訪問數據源的關鍵字,路由到指定數據源,獲取連接

1.pom.xml導入相關坐標

<!--mysql相關-->
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <scope>runtime</scope>
</dependency>
<!--oracle相關-->
<dependency>
 <groupId>com.github.noraui</groupId>
 <artifactId>ojdbc7</artifactId>
 <version>${oracle.version}</version>

2.1application.properties配置多數據源

#多數據源配置
db.groups=default,oracle

#默認數據庫(mysql庫)
db.default.url=jdbc:mysql://127.0.0.1:3306/demo?connectTimeout=2000&allowMultiQueries=true&rewriteBatchedStatements=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
db.default.username=root
db.default.password=root

#oracle庫
db.oracle.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl
db.oracle.username=root
db.oracle.password=root

2.2application.yml添加datasource配置

spring:
 datasource:
    group: ${db.groups}

3.1數據源切換方法: 維護一個static變量datasourceContext用於記錄每個線程需要使用的數據源關鍵字。並提供切換、讀取、清除數據源配置信息的方法

編寫DataSourceContextHolder類

public class DataSourceContextHolder {
 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

 public static synchronized void setDataSourceKey(String key) {
 contextHolder.set(key);
    }

 public static String getDataSourceKey() {
 return contextHolder.get();
    }

 public static void clearDataSourceKey() {
 contextHolder.remove();
    }
}

3.2實現AbstractRoutingDataSource 

public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance; private static Object lock=new Object(); private static Map<Object,Object> dataSourceMap = Maps.newHashMap(); @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceKey(); } @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); dataSourceMap.putAll(targetDataSources); super.afterPropertiesSet(); } public static synchronized DynamicDataSource getInstance(){ if(instance==null){ synchronized (lock){ if(instance==null){ instance=new DynamicDataSource(); } } } return instance; } public static boolean isExistDataSource(String key) { if (StringUtils.isEmpty(key)) { return false; } return dataSourceMap.containsKey(key); } }

3.3編寫數據源配置類MybatisConfig

@Configuration
@MapperScan(basePackages = { "com.**.mapper"} , sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {

private static Logger LOG = LoggerFactory.getLogger(MybatisConfig.class);

@Autowired
private Environment environment;

private static final String DEFAULT_DATASOURCE_NAME = "default";

@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {

//需要引入mybatis-plus坐標
//MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();

SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);

//bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/wttech/vsm/support/mapper/*.xml"));
return bean.getObject();
}

@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}

@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}

@Bean(name = "dynamicDataSource")
public DynamicDataSource dynamicDataSource() {
String groups = environment.getProperty("spring.datasource.group");
LOG.info("數據源組名稱:{}", groups);
Map<Object,Object> dataSourceMap = Maps.newHashMap();
Set<String> dbNames = Arrays.asList(groups.split(",")).stream().filter(s -> s.trim().length() > 0).collect(Collectors.toSet());
dbNames.add(DEFAULT_DATASOURCE_NAME);
HikariDataSource first = null;
HikariDataSource def = null;
for (String dbName:dbNames) {
String driver = environment.getProperty(String.format("db.%s.driver", dbName));
String url = environment.getProperty(String.format("db.%s.url", dbName));
String username = environment.getProperty(String.format("db.%s.username", dbName));
String password = environment.getProperty(String.format("db.%s.password", dbName));
LOG.info("數據源{}連接:{}", dbName, url);
if (StringUtils.isEmpty(url) || StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
continue;
}
DataSourceBuilder<HikariDataSource> hikariDataSourceBuilder = DataSourceBuilder.create().type(HikariDataSource.class);
if (!StringUtils.isEmpty(driver)) {
hikariDataSourceBuilder.driverClassName(driver);
}
HikariDataSource hikariDataSource = hikariDataSourceBuilder.url(url).username(username).password(password).build();
hikariDataSource.setAutoCommit(true);
String testQuery = environment.getProperty(String.format("db.%s.connectionTestQuery", dbName));
if (!StringUtils.isEmpty(testQuery)) {
hikariDataSource.setConnectionTestQuery(testQuery);
}
String timeout = environment.getProperty(String.format("db.%s.connectionTimeout", dbName));
if (!StringUtils.isEmpty(timeout)) {
hikariDataSource.setConnectionTimeout(Long.parseLong(timeout));
}
String minimumIdle = environment.getProperty(String.format("db.%s.minimumIdle", dbName));
if (!StringUtils.isEmpty(minimumIdle)) {
hikariDataSource.setMinimumIdle(Integer.parseInt(minimumIdle));
}
String maximumPoolSize = environment.getProperty(String.format("db.%s.maximumPoolSize", dbName));
if (!StringUtils.isEmpty(maximumPoolSize)) {
hikariDataSource.setMaximumPoolSize(Integer.parseInt(maximumPoolSize));
}
String idleTimeout = environment.getProperty(String.format("db.%s.idleTimeout", dbName));
if (!StringUtils.isEmpty(idleTimeout)) {
hikariDataSource.setIdleTimeout(Long.parseLong(idleTimeout));
}
String maxLifetime = environment.getProperty(String.format("db.%s.maxLifetime", dbName));
if (!StringUtils.isEmpty(maxLifetime)) {
hikariDataSource.setMaxLifetime(Long.parseLong(maxLifetime));
}
hikariDataSource.setPoolName(dbName);
dataSourceMap.put(dbName, hikariDataSource);
if (first == null) {
first = hikariDataSource;
}
if (DEFAULT_DATASOURCE_NAME.equals(dbName)) {
def = hikariDataSource;
}
}
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
dynamicDataSource.setTargetDataSources(dataSourceMap);
dynamicDataSource.setDefaultTargetDataSource(def == null ? first : def);
return dynamicDataSource;
}

4.1.標記數據源注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchDataSource {
 String value();
}

4.2.編寫切入點方法

@Aspect
@Component
public class MethodInterceptor {
@Around("execution(* com.wttech.vsm.support.mapper..*.*(..)))
public Object dao(ProceedingJoinPoint invocation) throws Throwable {
 MethodSignature methodSignature = (MethodSignature) invocation.getSignature();
    Method method = methodSignature.getMethod();

    String dbName = null;
    SwitchDataSource dataSource = method.getAnnotation(SwitchDataSource.class);
    if (dataSource != null) {
 dbName = dataSource.value();
        if (DynamicDataSource.isExistDataSource(dbName)) {
 DataSourceContextHolder.setDataSourceKey(dbName);
        }
 }
 try {
 return invocation.proceed();
    } finally {
 DataSourceContextHolder.clearDataSourceKey();
    }
} 
}

 

二.單庫事務控制:

本人涉及到到的業務場景是在service層的一個方法中存在多個dao操作(只涉及單庫),,需要維持事務性,,,遇到問題: 數據源是在mapper層通過注解切換的,,@Transactional在services層控制,,導致程序報錯找不到數據源,,開始的解決思路是前提@SwitchDataSource注解至service層 ,,但經測試后還是報錯,,,

最后的解決思路是必須保證切換數據源是在事務控制開啟之前完成...

1.切入點表達式增加service層切入

@Around("execution(* com.**.mapper..*.*(..)) || execution(* com.**.service.DataFixService.*(..))")

2.注釋掉需要事務控制的dao操作設計到的mapper層的數據源切換注解

// @SwitchDataSource("toll")
 int updateFixInList(@Param("listId") String listId, @Param("fieldName") String fieldName, @Param("fieldValue") String fieldValue);

3.事務控制是基於數據源的,,必須在數據源切換后在開啟事務,,以下是具體實現思路:

控制器->方法1(切換數據源,使用代理方式調用方法2)->方法2(開啟事務,執行多個dao操作)

先切換數據源

@SwitchDataSource("toll")
public void updateFixInList(String listId, String fieldName, String fieldValue) {

 //springAOP的用法中,只有代理的類才會被切入,我們在controller層調用service的方法的時候,是可以被切入的,但是如果我們在service層 A方法中,調用B方法,切點切的是B方法,那么這時候是不會切入的
 //通過((Service)AopContext.currentProxy()).B() 來調用B方法,這樣一來,就能切入了!
 ((DataFixService) AopContext.currentProxy()).updateFixInListProxy(listId, fieldName, fieldValue);
}

再控制事務

@Transactional(rollbackFor = Exception.class)
public void updateFixInListProxy(String listId, String fieldName, String fieldValue) {

 dataFixMapper.updateFixInList(listId, fieldName, fieldValue);

  //其他dao操作
}

特別注意: 以上這種方法目前僅適用於多數據源下單庫的事務操作,,,如果serviece方法dao操作設計多庫,,由於目前業務場景暫未涉及到,,所以暫未深入研究.....

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM