原理

DataSource向外提供一個 getConnection() 方法,得getConnection者得資料庫
AbstractRoutingDataSource 實作了 getConnection() 方法
// line 166
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
... 省略若幹代碼
// line 190
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
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 + "]");
}
return dataSource;
}
@Nullable
protected abstract Object determineCurrentLookupKey();
然而 ....
AbstractRoutingDataSource 的getConnection() 方法隻是調用了 determinTargetDataSource().getConnection() 來擷取真正DataSource的getConnection()。
這是典型的裝飾模式!!自己沒有的功能通過引入其他類來增強。
我們先來看看 AbstractRoutingDataSource 的類結構
被框框套住的都是重要的。
方法determineCurrentLookupKey() 是留給我們開發者的(就像你家的網線口),我們通過實作該方法在不同資料源之間切換。
實踐
1. 配置多資料源
在 application.yml 如下配置
spring:
datasource:
# 資料源類型
type: com.alibaba.druid.pool.DruidDataSource
# 預設資料源
default-datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
username: root
password: 123456
# 多資料源
target-datasources:
datasource1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
username: root
password: 123456
datasource2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
username: root
password: 123456
# druid 預設配置
druid:
# 初始連接配接數
initial-size: 10
# 最大連接配接池數量
max-active: 100
# 最小連接配接池數量
min-idle: 10
# 配置擷取連接配接等待逾時的時間
max-wait: 60000
# 打開PSCache,并且指定每個連接配接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接配接,機關是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接配接在池中最小生存的時間,機關是毫秒
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /monitor/druid*Mapper.xml
# 加載全局的配置檔案
configLocation: classpath:mybatis-config.xml
此處配置的名稱(如 defaultDataSource、targetDataSources)的命名并無特殊要求,隻要和下面第n步的 DataSourceConfig 中對應起來就可以
使用 Druid 資料源的話,要在 pom.xml 中引入依賴
com.alibaba
druid-spring-boot-starter
1.1.10
2. 實作動态資料源
DynamicDataSource 動态資料源,在多個資料源之間切換
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
DataSourceContextHolder 資料源上下文,使用線程變量來存儲代表目前使用的資料源的key值(每個key值都對應一個資料源,用以區分多資料源)
public class DataSourceContextHolder {
public static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal();
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void removeDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
DataSourceType 資料源對應的key(其實單純的用字元串來表示資料源,替換枚舉類DataSourceType也是可以的,但是寫代碼時要注意字元串統一)
public enum DataSourceType {
DEFAULT_DATASOURCE,
DATASOURCE1,
DATASOURCE2;
}
3. 将資料源添加到 Spring 容器中
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.default-datasource")
public DataSource defaultDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource1")
public DataSource dataSource1() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource2")
public DataSource dataSource2() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource(DataSource defaultDataSource, DataSource dataSource1, DataSource dataSource2) {
// 注意:該方法的參數名稱要和前面前面三個datasource對象在Spring容器中的bean名稱一樣
// 或者使用 @Qualifier 指定具體的bean
Map targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.DEFAULT_DATASOURCE.name(), defaultDataSource);
targetDataSources.put(DataSourceType.DATASOURCE1.name(), dataSource1);
targetDataSources.put(DataSourceType.DATASOURCE2.name(), dataSource2);
return new DynamicDataSource(defaultDataSource, targetDataSources);
}
}
測試
為了友善,省略了 Service 層
TestController
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestMapper testMapper;
@GetMapping
public List> test(String dataSourceIndex) {
// 根據參數值的不同,切換資料源
if ("1".equals(dataSourceIndex)) {
DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE1.name());
} else if ("2".equals(dataSourceIndex)) {
DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE2.name());
}
List> mapList = testMapper.selectList();
// 清除線程内部變量資料源key
DataSourceContextHolder.removeDataSourceType();
return mapList;
}
}
TestMapper
@Repository
public interface TestMapper {
List> selectList();
}
TestMapper.xml
SELECT * FROM test
别忘了要準備資料哦!
下面SQL語句,建立3個資料庫,然後在3個資料庫中都建立一張test表,并各自插入不同的資料。
-- 建立資料庫
create database db0 character set utf8 collate utf8_general_ci;
create database db1 character set utf8 collate utf8_general_ci;
create database db2 character set utf8 collate utf8_general_ci;
-- 在資料庫db1下執行以下SQL
use db0;
create table test(
id int(11) primary key auto_increment,
name varchar(20)
) ;
insert into test(name) values('張三');
-- 在資料庫db1下執行以下SQL
use db1;
create table test(
id int(11) primary key auto_increment,
name varchar(20)
) ;
insert into test(name) values('李四');
-- 在資料庫db2下執行以下SQL
use db2;
create table test(
id int(11) primary key auto_increment,
name varchar(20)
) ;
insert into test(name) values('王五');
OK,一切準備就緒,啟動應用吧!!!
一啟動就出現了各種各樣的,似乎無窮無盡的報錯!一頭黑線。
1. 找不到TestMapper
Field testMapper in com.liuchuanv.dynamicdatasource.controller.TestController required a bean of type 'com.liuchuanv.dynamicdatasource.mapper.TestMapper' that could not be found.
解決方法:在 DynamicdatasourceApplication 頭上添加注解 @MapperScan("com.liuchuanv.*.mapper")
2. dynamicDataSource 依賴循環
┌─────┐
| dynamicDataSource defined in class path resource [com/liuchuanv/dynamicdatasource/common/DataSourceConfig.class]
↑ ↓
| defaultDataSource defined in class path resource [com/liuchuanv/dynamicdatasource/common/DataSourceConfig.class]
↑ ↓
| org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
└─────┘
解決方法:在 DynamicdatasourceApplication 頭上修改注解 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
使用的是預設資料源 defaultDataSource
使用的是資料源 dataSource1
使用的是資料源 dataSource2
建議大家在心裡總結一下整個的過程,其實很簡單