1 背景
業務開發中,後端通常需要操作多個數據庫(可能同類型,也可能不同類型)中的數據,比如主、從數據庫的切換場景通常就是同類型切換。但實際需求中,也有可能需要不同類型數據庫之間的切換。不論是否同類型,其背後原理一致,只需在配置文件中修改數據庫驅動即可。
springboot 提供的AbstractRoutingDataSource實現多數據源動態切換的核心邏輯是:通過AOP的方式在程序運行時,把數據源通過 AbstractRoutingDataSource 動態織入到程序中,靈活地進行數據源切換。本文記錄了利用AbstractRoutingDataSource實現在service層通過註解的方式對Mysql和Postgresql兩種數據庫動態切換。
2 基本配置及maven依賴
首先在application.yml文件中添加數據源配置
spring:
profiles:
active: @pom.env@
http:
encoding:
charset: utf-8
force: true
enabled: true
mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.cetiti.test.model
application-dev.yml開發環境配置:
spring:
datasource:
db1:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://10.0.40.70:5432/common_account
username: postgres
password: 123456
db2:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://10.0.30.232:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
maven依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
3 代碼實現
3.1 編寫數據源配置類
姑且命名爲:MyBatisConfig
@Configuration
public class MyBatisConfig {
/**
* @return
* @throws Exception
* @Primary 必需指定一個且只能有一個主數據源,否則報錯
*/
@Primary
@Bean("mysql")
@ConfigurationProperties(prefix = "spring.datasource.db1")//根據數據源前綴到application.yml讀取數據源信息
public DataSource masterDataSource() throws Exception {
return DataSourceBuilder.create().build();
}
@Bean("postgresql")
@ConfigurationProperties(prefix = "spring.datasource.db2")//根據數據源前綴到application.yml讀取數據源信息//可以配置更多數據源,到前提是application.yml中存在,而且也需要在枚舉類中添加枚舉類型
public DataSource slaverDataSource() throws Exception {
return DataSourceBuilder.create().build();
}
/**
* @Qualifier 根據名稱進行注入,通常是在具有相同的多個類型的實例的一個注入(例如有多個DataSource類型的實例)
* @DataSourceTypeAnno(DataSourceEnum.MASTER)事務方法需要指定數據源
*/
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("mysql") DataSource masterDataSource,
@Qualifier("postgresql") DataSource slaverDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(4);
targetDataSources.put(DataSourceEnum.MYSQL, masterDataSource);
targetDataSources.put(DataSourceEnum.POSTGRESQL, slaverDataSource);
DynamicDataSource dataSource = new DynamicDataSource();
// 該方法是AbstractRoutingDataSource的方法
dataSource.setTargetDataSources(targetDataSources);
// 默認的datasource設置爲myTestDbDataSource
dataSource.setDefaultTargetDataSource(masterDataSource);
return dataSource;
}
/**
* 根據數據源創建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource,
@Value("mybatis.type-aliases-package") String typeAliasesPackage) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 指定數據源(這個必須有,否則報錯)
factoryBean.setDataSource(dynamicDataSource);
// 下邊兩句僅僅用於*.xml文件,如果整個持久層操作不需要使用到xml文件的話(只用註解就可以搞定),則不加
// 指定實體類所在的包 //掃描mapper.xml文件包
//factoryBean.setTypeAliasesPackage(typeAliasesPackage);
//factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/**/*Mapper.xml"));
return factoryBean.getObject();
}
/**
* 配置事務管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
}
3.2 數據源標識枚舉類:
public enum DataSourceEnum {
// 主
MYSQL,
//備
POSTGRESQL;
}
3.3 添加AbstractRoutingDataSource實現類
後續在獲取數據源標識時會回調該類中的determineCurrentLookupKey方法:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//自定義數據源標識上下文容器DataSourceContextHolder,用於存放各標識
return DataSourceContextHolder.getDataSourceType();
}
}
通過追蹤源碼可見,AbstractRoutingDataSource的getConnection() 方法根據查找 lookupkey 鍵對不同目標數據源的調用,通常是通過某些線程綁定的事務上下文來實現。
實現邏輯:
定義DynamicDataSource類繼承抽象類AbstractRoutingDataSource,並實現了determineCurrentLookupKey()方法。
把配置的多個數據源放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然後通過afterPropertiesSet()方法將數據源分別進行復制到resolvedDataSources和resolvedDefaultDataSource中。
調用AbstractRoutingDataSource的getConnection()的方法的時候,先調用determineTargetDataSource()方法返回DataSource,再進行getConnection(),determineTargetDataSource()源碼如下:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//獲取目標數據源鍵值
Object lookupKey = this.determineCurrentLookupKey();
//根據鍵值,獲取數據源
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
//如果沒有獲取到,證明不存在,則獲取默認數據源
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
3.4 定義數據源標識上下文容器,
本質上還是利用ThreadLocal實現數據安全
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceEnum> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceEnum.MYSQL);
public static void setDataSourceType(DataSourceEnum type) {
CONTEXT_HOLDER.set(type);
}
public static DataSourceEnum getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void resetDataSourceType() {
CONTEXT_HOLDER.set(DataSourceEnum.MYSQL);
}
}
3.5 自定義註解:
@Retention(RetentionPolicy.RUNTIME)
// 註解可以用在方法上
@Target(ElementType.METHOD)
public @interface DataSourceTypeAnno {
//使用方式在service層方法上添加@DataSourceTypeAnno(DataSourceEnum.數據源枚舉類型)用於指定所使用的數據源
DataSourceEnum value() default DataSourceEnum.MYSQL;
}
3.6 定義切面類:
@Component
@Aspect
@Order(-100)
public class DataSourceAspect {
//這裏掃描service層方法上的自定義註解,去判斷所使用的數據源類型,並動態切換數據源
@Pointcut("execution(* com.cetiti.*.*..*(..)) " +
"&& @annotation(com.cetiti.rm.common.annotation.DataSourceTypeAnno)")
public void dataSourcePointcut() {
}
@Around("dataSourcePointcut()")
public Object doAround(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
DataSourceTypeAnno typeAnno = method.getAnnotation(DataSourceTypeAnno.class);
DataSourceEnum sourceEnum = typeAnno.value();
if (sourceEnum == DataSourceEnum.MYSQL) {
DataSourceContextHolder.setDataSourceType(DataSourceEnum.MYSQL);
} else if (sourceEnum == DataSourceEnum.POSTGRESQL) {
DataSourceContextHolder.setDataSourceType(DataSourceEnum.POSTGRESQL);
}
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
DataSourceContextHolder.resetDataSourceType();
}
return result;
}
}
4 測試
4.1 新建表
在不同類型數據庫下新建表如下:
MySQL:
PostgreSQL:
4.2 service層代碼
@Service
public class StudentServiceImpl implements StudentService {
@Resource
private MyDbTestMapper myDbTestMapper;
@Override
@DataSourceTypeAnno(value = DataSourceEnum.MYSQL)
public List<Student> getMasterStudent() {
return myDbTestMapper.getStudent();
}
@Override
@DataSourceTypeAnno(value = DataSourceEnum.POSTGRESQL)
public List<Student> getSlaveStudent() {
return myDbTestMapper.getStudent();
}
}
4.3 mapper層代碼:
@Mapper
public interface MyDbTestMapper {
/**
* 獲取任務狀態字典表信息
* @return list
*/
@Select("select s_id as id,s_name as enName,t_id as tid,\n"+
"real_name as cnName,sex,address,enroll_score as enrollScore,tel from student order by enroll_score")
List<Student> getStudent();
}
4.4 controller層代碼:
@GetMapping("/test/students")
@ApiOperation("2.1 獲取學生信息")
public List<Student> getMyStudent() {
List<Student> list = studentService.getMasterStudent();
System.out.println(list);
list = studentService.getSlaveStudent();
System.out.println(list);
return list;
}
在測試之前還需要在應用主類中添加mapper掃描
@SpringBootApplication
@MapperScan("com.cetiti.test.mapper")
public class RedmineBackendApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
可能會遇到測試異常情況:
這事由於mysql版本過低所致,更改至5.5.*以上即可。
正常測試結果:
注:基於AbstractRoutingDataSource的多數據源動態切換,可以實現主備切換、讀寫分離,這麼做缺點也很明顯,無法動態的增加數據源。