最近項目用到了
spring
多數據源配置,恰好看到這篇好文章,特地分享下
點擊了解Spring多數據源XML配置
1 SpringBoot分庫配置
主要介紹兩種整合方式,分別是 springboot+mybatis
使用分包方式整合,和 springboot+druid+mybatisplus
使用注解方式整合
1.1 准備數據
在本地新建兩個數據庫,名稱分別為db1
和db2
,新建一張user
表,表結構如下
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`name` varchar(25) NOT NULL COMMENT '姓名',
`age` int(2) DEFAULT NULL COMMENT '年齡',
`sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性別:0-男,1-女',
`addr` varchar(100) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='測試用戶表'
1.2 springboot+mybatis使用分包方式整合
1.2.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>multipledatasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>multipledatasource</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql 依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2.2 application.yml 配置文件
server:
port: 8080 # 啟動端口
spring:
datasource:
db1: # 數據源1
jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 數據源2
jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
注意事項:
各個版本的 springboot
配置 datasource
時參數有所變化,例如低版本配置數據庫 url
時使用 url
屬性,高版本使用 jdbc-url
屬性,請注意區分
1.2.3 連接數據源配置文件
1.2.3.1 連接源配置一
@Configuration
@MapperScan(basePackages = "com.example.multipledatasource.mapper.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
public class DataSourceConfig1 {
@Primary // 表示這個數據源是默認數據源, 這個注解必須要加,因為不加的話spring將分不清楚那個為主數據源(默認數據源)
@Bean("db1DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db1") //讀取application.yml中的配置參數映射成為一個對象
public DataSource getDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Primary
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapper的xml形式文件位置必須要配置,不然將報錯:no statement (這種錯誤也可能是mapper的xml中,namespace與項目的路徑不一致導致)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/db1/*.xml"));
return bean.getObject();
}
@Primary
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
1.2.3.2 連接源配置二
@Configuration
@MapperScan(basePackages = "com.example.multipledatasource.mapper.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class DataSourceConfig2 {
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource getDb1DataSource(){
return DataSourceBuilder.create().build();
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/db2/*.xml"));
return bean.getObject();
}
@Bean("db2SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
1.2.4 項目結構
注意事項:
在 service
層中根據不同的業務注入不同的 dao
層
如果是主從復制- -讀寫分離:比如 db1
中負責增刪改,db2
中負責查詢。但是需要注意的是負責增刪改的數據庫必須是主庫(master)
1.3 springboot+druid+mybatisplus使用注解整合
1.3.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mutipledatasource2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mutipledatasource2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>local1</id>
<properties>
<profileActive>local1</profileActive>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>local2</id>
<properties>
<profileActive>local2</profileActive>
</properties>
</profile>
</profiles>
</project>
1.3.2 application.yml 配置文件
server:
port: 8080
spring:
datasource:
dynamic:
primary: db1 # 配置默認數據庫
datasource:
db1: # 數據源1配置
url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
db2: # 數據源2配置
url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 1
max-active: 20
min-idle: 1
max-wait: 60000
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 去除druid配置
DruidDataSourceAutoConfigure
會注入一個DataSourceWrapper
,其會在原生的spring.datasource
下找 url, username, password
等。動態數據源 URL
等配置是在 dynamic
下,因此需要排除,否則會報錯。排除方式有兩種,一種是上述配置文件排除
,還有一種可以在項目啟動類排除
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1.3.3 使用@DS區分數據源
給使用非默認數據源
添加注解@DS
@DS
可以注解在 方法
上和 類
上,同時存在方法注解優先於類上注解。
注解在 service
實現或 mapper
接口方法上,不要同時在 service
和 mapper
注解
mapper上使用
@DS("db2")
public interface UserMapper extends BaseMapper<User> {
}
service上使用
@Service
@DS("db2")
public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements IModelService {}
方法上使用
@Select("SELECT * FROM user")
@DS("db2")
List<User> selectAll();
轉載於:https://www.cnblogs.com/aizen-sousuke/p/11756279.html
1.3.4 @Transaction和@DS問題
點擊了解當一起使用@Transaction和@DS時@DS失效問題
1.4 讀寫分離庫
讀寫分離庫使用AbstractRoutingDataSource
AbstractRoutingDataSource
類和aop
結合,還可以用來作為讀寫分離庫
1.4.1 AbstractRoutingDataSource原理
AbstractRoutingDataSource
原理:
Map<Object, Object> targetDataSources
保存了所有可能的數據源,key
為數據庫的key
,value
為DataSource
對象或字符串形式的連接信息Object defaultTargetDataSource
保存了默認的數據源,用於找不到具體的數據源時使用afterPropertiesSet()
方法
解析targetDataSources
數據源信息成<key,DataSource>
的形式,保存在Map<Object, DataSource> resolvedDataSources
中
將defaultTargetDataSource
中的默認數據源信息解析成DataSource
對象保存在DataSource resolvedDefaultDataSource
中determineCurrentLookupKey()
提供給子類重寫,指定當前線程使用的具體的數據源的key
determineTargetDataSource()
根據determineCurrentLookupKey()
方法返回的key
返回數據源DataSouce
對象,若沒有,則使用默認數據源對象getConnection()
根據determineTargetDataSource()
返回的數據源,與其建立連接
1.4.2 ThreadLocal工具類
創建一個類用於操作 ThreadLocal
,主要是通過get,set,remove
方法來獲取、設置、刪除當前線程對應的數據源。
public class DataSourceContextHolder {
//此類提供線程局部變量。這些變量不同於它們的正常對應關系是每個線程訪問一個線程(通過get、set方法),有自己的獨立初始化變量的副本。
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 設置數據源
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 獲取當前線程的數據源
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* 刪除當前數據源
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}
1.4.3 繼承AbstractRoutingDataSource
定義一個動態數據源繼承 AbstractRoutingDataSource
,通過determineCurrentLookupKey
方法與上述實現的ThreadLocal
類中的get
方法進行關聯,實現動態切換數據源。
/**
* @description: 實現動態數據源,根據AbstractRoutingDataSource路由到不同數據源中
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
上述代碼中,還實現了一個動態數據源類的構造方法,主要是為了設置默認數據源,以及以Map
保存的各種目標數據源。其中Map
的key
是設置的數據源名稱,value
則是對應的數據源(DataSource)
1.4.4 配置數據源
配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxx
password: xxxx
driver-class-name: com.mysql.cj.jdbc.Driver
配置類
/**
* @description: 設置數據源
**/
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master",defaultDataSource);
dataSourceMap.put("slave",slaveDataSource());
return new DynamicDataSource(defaultDataSource,dataSourceMap);
}
}
通過配置類,將配置文件中的配置的數據庫信息轉換成datasource
,並添加到DynamicDataSource
中,同時通過@Bean
將DynamicDataSource
注入Spring
中進行管理,后期在進行動態數據源添加時,會用到。
1.4.5 測試
我們創建一個getData的方法,參數就是需要查詢數據的數據源名稱。
@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
DataSourceContextHolder.setDataSource(datasourceName);
TestUser testUser = testUserMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return testUser.getUserName();
}
在上述代碼中,我們看到DataSourceContextHolder.setDataSource(datasourceName);
來設置了當前線程需要查詢的數據庫,通過DataSourceContextHolder.removeDataSource();
來移除當前線程已設置的數據源。
使用過Mybatis-plus
動態數據源的小伙伴,應該還記得我們在使用切換數據源時會使用到DynamicDataSourceContextHolder.push(String ds);
和DynamicDataSourceContextHolder.poll();
這兩個方法,翻看源碼我們會發現其實就是在使用ThreadLocal
時使用了棧,這樣的好處就是能使用多數據源嵌套
注:啟動程序時,小伙伴不要忘記將SpringBoot自動添加數據源進行排除哦,否則會報循環依賴問題。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
1.4.6 使用自定義注解
在上述中,雖然已經實現了動態切換數據源,但是我們會發現如果涉及到多個業務進行切換數據源的話,我們就需要在每一個實現類中添加這一段代碼。假如使用注解來進行優化呢,如下
1.4.6.1 定義注解
我們就用mybatis
動態數據源切換的注解:DS
,代碼如下:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}
1.4.6.2 實現aop
@Aspect
@Component
@Slf4j
public class DSAspect {
@Pointcut("@annotation(com.test.dynamic_datasource.dynamic.aop.DS)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)){
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}
代碼使用了@Around
,通過ProceedingJoinPoint
獲取注解信息,拿到注解傳遞值,然后設置當前線程的數據源。
1.4.7 動態添加數據源
業務場景 :有時候我們的業務會要求我們從保存有其他數據源的數據庫表中添加這些數據源,然后再根據不同的情況切換這些數據源。因此我們需要改造下 DynamicDataSource
來實現動態加載數據源。
1.4.7.1 數據源實體
@Data
@Accessors(chain = true)
public class DataSourceEntity {
/**
* 數據庫地址
*/
private String url;
/**
* 數據庫用戶名
*/
private String userName;
/**
* 密碼
*/
private String passWord;
/**
* 數據庫驅動
*/
private String driverClassName;
/**
* 數據庫key,即保存Map中的key
*/
private String key;
}
實體中定義數據源的一般信息,同時定義一個key
用於作為DynamicDataSource
中Map
中的key
1.4.7.2 修改DynamicDataSource
/**
* @description: 實現動態數據源,根據AbstractRoutingDataSource路由到不同數據源中
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object,Object> targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
/**
* 添加數據源信息
* @param dataSources 數據源實體集合
* @return 返回添加結果
*/
public Boolean createDataSource(List<DataSourceEntity> dataSources){
try {
if (CollectionUtils.isNotEmpty(dataSources)){
for (DataSourceEntity ds : dataSources) {
//校驗數據庫是否可以連接
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(),ds.getUserName(),ds.getPassWord());
//定義數據源
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds,dataSource);
//申請連接時執行validationQuery檢測連接是否有效,這里建議配置為TRUE,防止取到的連接不可用
dataSource.setTestOnBorrow(true);
//建議配置為true,不影響性能,並且保證安全性。
//申請連接的時候檢測,如果空閑時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
dataSource.setTestWhileIdle(true);
//用來檢測連接是否有效的sql,要求是一個查詢語句。
dataSource.setValidationQuery("select 1 ");
dataSource.init();
this.targetDataSourceMap.put(ds.getKey(),dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
// 將TargetDataSources中的連接信息放入resolvedDataSources管理
super.afterPropertiesSet();
return Boolean.TRUE;
}
}catch (ClassNotFoundException | SQLException e) {
log.error("---程序報錯---:{}", e.getMessage());
}
return Boolean.FALSE;
}
/**
* 校驗數據源是否存在
* @param key 數據源保存的key
* @return 返回結果,true:存在,false:不存在
*/
public boolean existsDataSource(String key){
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}
在改造后的DynamicDataSource
中,我們添加可以一個 private final Map<Object,Object> targetDataSourceMap
,這個map
會在添加數據源的配置文件時將創建的Map
數據源信息通過DynamicDataSource
構造方法進行初始賦值,即:DateSourceConfig
類中的createDynamicDataSource()
方法中。
同時我們在該類中添加了一個createDataSource
方法,進行數據源的創建,並添加到map
中,再通過super.setTargetDataSources(this.targetDataSourceMap);
進行目標數據源的重新賦值
1.4.7.3 動態添加數據源
上述代碼已經實現了添加數據源的方法,那么我們來模擬通過從數據庫表中添加數據源,然后我們通過調用加載數據源的方法將數據源添加進數據源Map
中。
在主數據庫中定義一個數據庫表,用於保存數據庫信息。
create table test_db_info(
id int auto_increment primary key not null comment '主鍵Id',
url varchar(255) not null comment '數據庫URL',
username varchar(255) not null comment '用戶名',
password varchar(255) not null comment '密碼',
driver_class_name varchar(255) not null comment '數據庫驅動'
name varchar(255) not null comment '數據庫名稱'
)
為了方便,我們將之前的從庫錄入到數據庫中,修改數據庫名稱。
insert into test_db_info(url, username, password,driver_class_name, name)
value ('jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
'root','123456','com.mysql.cj.jdbc.Driver','add_slave')
1.4.7.4 啟動添加配置
啟動SpringBoot
時添加數據源:
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbInfoMapper testDbInfoMapper;
@Override
public void run(String... args) throws Exception {
List<TestDbInfo> testDbInfos = testDbInfoMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDbInfos)) {
List<DataSourceEntity> ds = new ArrayList<>();
for (TestDbInfo testDbInfo : testDbInfos) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDbInfo,sourceEntity);
sourceEntity.setKey(testDbInfo.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}