Spring Boot 實現數據源動態切換的最佳姿勢!
一、背景介紹
隨著用戶需求的不斷變化,在很多的業務環境下,我們需要用到動態數據源,比如多租戶的場景,簡單的說就是,多個租戶除了數據源不一樣,服務代碼和數據表結構完全一致。這個時候采用動態數據源方案,可以極大的簡化服務工程量。
圖片
在介紹動態數據源之前,我們先一起來看看多數據源在 Spring Boot 中的實現方式。
二、多數據源實現介紹
服務框架采用 Spring Boot + Mybatis + Druid 來實現數據庫的訪問和操作,數據庫采用 Mysql 來存儲和查詢,程序環境如下:
- Mysql:5.7
- JDK:1.8
- Spring Boot:2.1.0
- Mybatis:3.5.0
- Druid:1.1.10
2.1、數據庫準備
為了便于演示,在 Mysql 數據庫中創建兩個庫,分別是db_test_1和db_test_2。
在db_test_1數據庫中創建一張用戶表,腳本如下:
CREATE TABLE`tb_user_info` (
`id`int(11) unsignedNOTNULL,
`user_name`varchar(50) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
在db_test_2數據庫中創建另一張賬戶表,腳本如下:
CREATE TABLE`tb_account_info` (
`id`int(11) unsignedNOTNULL,
`account_name`varchar(50) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;
創建完成之后,確保數據庫的 IP、端口、用戶和密碼,能遠程正常聯通。
2.2、工程環境準備
以Spring Boot框架為基礎,創建一個服務工程,并在pom.xml中添加相關的依賴包,示例如下:
<!--spring boot核心-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring boot 測試-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mysql 驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!--aspectj 注解代理-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
2.3、編寫多數據源配置服務
Spring Boot 支持根據一定的規則來動態選擇數據源,用戶可以通過繼承AbstractRoutingDataSource抽象類并重寫determineCurrentLookupKey()方法來完成數據源的切換。
在每次執行數據庫操作之前,它會先調用determineCurrentLookupKey()抽象方法,根據初始化時設置的數據源集合,通過其中的key來決定使用哪個數據源。
具體實現方式如下!
2.3.1、創建動態數據源服務類
首先,創建一個DynamicDataSource類,并繼承AbstractRoutingDataSource抽象類,同時重寫determineCurrentLookupKey()方法,代碼示例如下:
package com.example.dynamic.datasource.config;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
2.3.2、創建動態數據源緩存類
然后,創建一個DataSourceContextHolder類,用于緩存數據源,同時需要確保線程環境下安全,這里采用ThreadLocal線程本地變量來實現數據源key的緩存,代碼示例如下:
package com.example.dynamic.datasource.config;
publicclass DataSourceContextHolder {
/**
* 設置線程獨立變量,用于存儲數據源唯一標記
*/
privatestaticfinal ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 設置數據源
* @param dataSourceName 數據源名稱
*/
public static void set(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 獲取當前線程的數據源
* @return 數據源名稱
*/
public static String get(){
return DATASOURCE_HOLDER.get();
}
/**
* 刪除當前數據源
*/
public static void remove(){
DATASOURCE_HOLDER.remove();
}
}
2.3.3、創建動態數據源配置類
接著,創建一個DataSourceConfig配置類,設置動態數據源相關的參數,并注入到 Bean 工廠,代碼示例如下:
package com.example.dynamic.datasource.config;
@Configuration
publicclass DataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1.druid")
public DataSource db1(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2.druid")
public DataSource db2(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource createDynamicDataSource(){
// 配置數據源集合,其中key代表數據源名稱,DataSourceContextHolder中緩存的就是這個key
Map<Object,Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("db1",db1());
dataSourceMap.put("db2",db2());
// 注入動態數據源到bean工廠
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(db1());
// 設置動態數據源集
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
2.3.4、編寫相關配置變量
根據上面的配置變量,我們還需要在application.properties文件中添加相關的數據源變量,內容如下:
# 數據庫源1
spring.datasource.db1.druid.url=jdbc:mysql://localhost:3306/db_test_1
spring.datasource.db1.druid.username=root
spring.datasource.db1.druid.password=root
spring.datasource.db1.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db1.druid.initial-size=1
spring.datasource.db1.druid.min-idle=1
spring.datasource.db1.druid.max-active=5
spring.datasource.db1.druid.max-wait=60000
# 數據庫源2
spring.datasource.db2.druid.url=jdbc:mysql://localhost:3306/db_test_2
spring.datasource.db2.druid.username=root
spring.datasource.db2.druid.password=root
spring.datasource.db2.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db2.druid.initial-size=1
spring.datasource.db2.druid.min-idle=1
spring.datasource.db2.druid.max-active=5
spring.datasource.db2.druid.max-wait=60000
2.3.5、排除自動裝配數據源
默認情況下,Spring Boot 啟動的時候會自動加載數據源配置,因為我們沒有按照約定配置指定的數據源參數,當啟動服務的時候會報錯。
因此,需要在注解@SpringBootApplication類上排除自動裝配數據源配置,內容如下:
package com.example.dynamic.datasource;
@MapperScan("com.example.dynamic.datasource.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.3.6、業務相關服務類
最后基于上文數據庫中創建的表,編寫相關的entity、service和dao層代碼,以便于后續驗證數據源的切換操作。
以用戶表的新增操作為例,代碼如下:
package com.example.dynamic.datasource.service;
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public void insert(UserInfo entity){
userInfoMapper.insert(entity);
}
}
賬戶表的新增操作,代碼如下:
package com.example.dynamic.datasource.service;
@Service
public class AccountInfoService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Transactional
public void insert(AccountInfo entity){
accountInfoMapper.insert(entity);
}
}
entity和dao層代碼就不再貼進來了,比較簡單。
2.4、編寫單元測試
最后我們編寫一個單元測試,驗證一下代碼的正確性,示例如下:
package com.example.dynamic.datasource.junit;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceJunit {
@Autowired
private AccountInfoService accountInfoService;
@Autowired
private UserInfoService userInfoService;
@Test
public void testUserInfo(){
try {
// 1.設置需要切換的數據源
DataSourceContextHolder.set("db1");
// 2.執行插入操作
userInfoService.insert(new UserInfo(1, "張三"));
} finally {
// 3.操作完成之后,移除本地線程緩存的數據源
DataSourceContextHolder.remove();
}
}
@Test
public void testAccountInfo(){
try {
// 1.設置需要切換的數據源
DataSourceContextHolder.set("db2");
// 2.執行插入操作
accountInfoService.insert(new AccountInfo(1, "中國銀行"));
} finally {
// 3.操作完成之后,移除本地線程緩存的數據源
DataSourceContextHolder.remove();
}
}
}
運行單元測試,輸出結果如下:
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ff6efdc]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@78226c36] will be managed by Spring
==> Preparing: insert into tb_user_info(id, user_name) values(?, ?)
==> Parameters: 1(Integer), 張三(String)
<== Updates: 1
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ff6efdc]
...
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c9e07c6]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2b10ace9] will be managed by Spring
==> Preparing: insert into tb_account_info(id, account_name) values(?, ?)
==> Parameters: 1(Integer), 中國銀行(String)
<== Updates: 1
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c9e07c6]
從日志打印上可以清晰的看到,數據被成功的插入到對應的數據庫中。
2.5、利用切面代理類設置數據源
在上文中,我們采用的是手動方式來設置數據源,在實際的業務開發中,我們通常會采用切面代理類來設置數據源,以便簡化代碼復雜度。
實現過程如下。
2.5.1、創建數據源注解
首先,定義一個數據源注解來實現數據源的切換,同時配置一個默認的數據源名稱,代碼示例如下:
package com.example.dynamic.datasource.config.aop;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DbSource {
/**
* 數據源key值
* @return
*/
String value() default "db1";
}
2.5.2、編寫數據源代理類
接著,基于@DbSource注解,創建一個 AOP 代理類,所有配置該注解的方法都會被前后攔截,代碼示例如下:
package com.example.dynamic.datasource.config.aop;
@Order(1)
@Aspect
@Component
publicclass DbSourceAspect {
privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(DbSourceAspect.class);
@Pointcut("@annotation(com.example.dynamic.datasource.config.aop.DbSource)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
// 獲取要切換的數據源名稱
MethodSignature methodSignature = (MethodSignature)point.getSignature();
Method method = methodSignature.getMethod();
DbSource dbSource = method.getAnnotation(DbSource.class);
LOGGER.info("select dataSource:" + dbSource.value());
DataSourceContextHolder.set(dbSource.value());
try {
return point.proceed();
} finally {
DataSourceContextHolder.remove();
}
}
}
這里有一個很重要的配置就是@Order(1),表示代理類會優先被執行,值越低優先級越高。
當@DbSource和@Transactional注解同時應用在一個方法上時,如果不指定代理類的順序,當調用方法的時候,會出現事務異常的問題。
原因在于:@Transactional注解所在的代理類,默認優先級是Integer.MAX,也就是最后被執行;當自定義的代理類沒有配置優先級時,同樣默認值也是Integer.MAX,但是自定義的會排在后面執行,此時當調用標注事務注解的方法時,事務代理類會先執行,但是數據源并沒有被設置,會導致事務控制異常,這一點需要特別注意。
2.5.3、使用注解切換數據源
最后,在需要的方法上配置相關的數據源注解即可。
以用戶服務類為例,代碼示例如下:
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
@DbSource(value = "db1")
public void add(UserInfo entity){
userInfoMapper.insert(entity);
}
}
賬戶服務類,代碼示例如下:
@Service
public class AccountInfoService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Transactional
@DbSource(value = "db2")
public void add(AccountInfo entity){
accountInfoMapper.insert(entity);
}
}
2.5.4、編寫單元測試
最后,編寫一個單元測試方法,驗證代碼的正確性,代碼示例如下:
package com.example.dynamic.datasource.junit;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceJunit {
@Autowired
private AccountInfoService accountInfoService;
@Autowired
private UserInfoService userInfoService;
@Test
public void test(){
// 新增用戶
userInfoService.add(new UserInfo(1, "張三"));
// 新增賬戶數據
accountInfoService.add(new AccountInfo(1, "中國銀行"));
}
}
日志輸出結果如下:
圖片
可以看到,數據被正確的插入到對應的數據庫中,與預期一致。
可以發現,采用 aop 代理的方式來切換數據源,業務實現上會更加的靈活。
三、動態數據源配置介紹
在上文中,我們介紹了多數據源的配置實現方式,這種配置方式有一個不好的地方在于:配置文件都是寫死的。
能不能改成動態的加載數據源呢,答案是可以的!
下面我們一起來看看相關的具體實現方式。
3.1、數據庫準備
首先,我們需要準備一張數據源配置表。新建一個test_db數據庫,然后在數據庫中創建一張數據源配置表,腳本如下:
CREATE TABLE`tb_db_info` (
`id`int(11) unsignedNOTNULL AUTO_INCREMENT,
`db_name`varchar(50) DEFAULTNULL,
`db_url`varchar(200) DEFAULTNULL,
`driver_class_name`varchar(100) DEFAULTNULL,
`username`varchar(80) DEFAULTNULL,
`password`varchar(80) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3DEFAULTCHARSET=utf8mb4;
最后,初始化兩條數據,方便后續數據源的查詢。
INSERT INTO `tb_db_info` (`id`, `db_name`, `db_url`, `driver_class_name`, `username`, `password`)
VALUES
(1, 'db1', 'jdbc:mysql://localhost:3306/db_test_1', 'com.mysql.jdbc.Driver', 'root', 'root'),
(2, 'db2', 'jdbc:mysql://localhost:3306/db_test_2', 'com.mysql.jdbc.Driver', 'root', 'root');
3.2、修改全局配置文件
我們還是以上文介紹的工程為例,把之前自定義的配置參數刪除掉,重新基于 Spring Boot 約定的配置方式,添加相關的數據源參數,內容如下:
# 配置默認數據源
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
3.3、編寫相關的服務類
基于數據庫中tb_db_info表,編寫相關的查詢邏輯,代碼示例如下:
package com.example.dynamic.datasource.entity;
publicclass DbInfo {
/**
* 主鍵ID
*/
private Integer id;
/**
* 數據庫key,即保存Map中的key
*/
private String dbName;
/**
* 數據庫地址
*/
private String dbUrl;
/**
* 數據庫驅動
*/
private String driverClassName;
/**
* 數據庫用戶名
*/
private String username;
/**
* 數據庫密碼
*/
private String password;
// set、get方法等...
}
public interface DbInfoMapper {
List<DbInfo> findAll();
}
<?xml versinotallow="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dynamic.datasource.mapper.DbInfoMapper">
<select id="findAll" resultType="com.example.dynamic.datasource.entity.DbInfo">
select
id
,db_name as dbName
,db_url as dbUrl
,driver_class_name as driverClassName
,username
,password
from tb_db_info
order by id
</select>
</mapper>
3.4、修改動態數據源服務類
對DynamicDataSource類進行一些調整,代碼如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
/**
* 重新加載數據源集合
* @param dbList
*/
public void loadDataSources(List<DbInfo> dbList){
try {
Map<Object, Object> targetDataSourceMap = new HashMap<>();
for (DbInfo source : dbList) {
// 初始化數據源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(source.getDriverClassName());
dataSource.setUrl(source.getDbUrl());
dataSource.setUsername(source.getUsername());
dataSource.setPassword(source.getPassword());
dataSource.setInitialSize(1);
dataSource.setMinIdle(1);
dataSource.setMaxActive(5);
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery("select 1 ");
dataSource.init();
targetDataSourceMap.put(source.getDbName(), dataSource);
}
super.setTargetDataSources(targetDataSourceMap);
// 重新初始化resolvedDataSources對象
super.afterPropertiesSet();
} catch (Exception e){
e.printStackTrace();
}
}
}
3.5、修改動態數據源配置類
對DataSourceConfig類也需要進行一些調整,通過 Spring Boot 默認的數據源配置類初始化一個數據源實例對象,代碼如下:
@Configuration
publicclass DataSourceConfig {
@Autowired
private DataSourceProperties basicProperties;
/**
* 注入動態數據源
* @param dataSource
* @return
*/
@Bean
@Primary
public DynamicDataSource dynamicDataSource(){
// 獲取初始數據源
DataSource defaultDataSource = basicProperties.initializeDataSourceBuilder().build();
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("defaultDataSource", defaultDataSource);
// 注入動態數據源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
// 設置動態數據源集
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
3.6、配置啟動時加載數據源服務類
以上的配置調整完成之后,我們還需要配置一個服務啟動監聽類,將從數據庫中查詢到的數據配置信息加載到DynamicDataSource對象中,代碼示例如下:
@Component
publicclass LoadDataSourceRunner implements CommandLineRunner {
@Autowired
private DbInfoMapper dbInfoMapper;
@Autowired
private DynamicDataSource dynamicDataSource;
@Override
public void run(String... args) {
List<DbInfo> dbList = dbInfoMapper.findAll();
if(!CollectionUtils.isEmpty(dbList)){
dynamicDataSource.loadDataSources(dbList);
}
}
}
3.7、調整 SpringBootApplication 注解配置
以上的實現方式,因為啟動的時候,采用的是 Spring Boot 默認的數據源配置實現,因此無需排除DataSourceAutoConfiguration類,可以將相關參數移除掉。
package com.example.dynamic.datasource;
@MapperScan("com.example.dynamic.datasource.mapper")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.8、代碼測試
最后,重新運行單元測試方法,輸出結果如下:
圖片
從日志上可以清晰的看到,運行結果與預期一致!
四、小結
本文主要圍繞利用 Spring Boot 來實現動態數據源的加載,進行一次知識的整合和總結,如果描述不對的地方,歡迎大家留言指出!
五、參考
1.https://www.baeldung.com/spring-boot-configure-multiple-datasources
2.https://cloud.tencent.com/developer/article/2370197