多數(shù)據(jù)源可以理解為多數(shù)據(jù)庫鸟蟹,甚至可以是多個不同類型的數(shù)據(jù)庫泣洞,比如一個是MySql,一個是Oracle秀仲。隨著項目的擴(kuò)大融痛,有時需要數(shù)據(jù)庫的拆分或者引入另一個數(shù)據(jù)庫,這時就需要配置多個數(shù)據(jù)源神僵。
SpringBoot中使用多數(shù)據(jù)源還是比較簡單的雁刷,為了演示方便,我們在MySql中創(chuàng)建兩個數(shù)據(jù)庫:ds1保礼、ds2沛励,并在ds1數(shù)據(jù)庫中創(chuàng)建student表,在ds2數(shù)據(jù)庫中創(chuàng)建teacher表炮障。數(shù)據(jù)庫腳本如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('123456', 'zhangsan', '北京');
INSERT INTO `student` VALUES ('123457', 'lisi', '上海');
SET FOREIGN_KEY_CHECKS = 1;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` varchar(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`class` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES ('0000001', 'wangwu', '上海');
SET FOREIGN_KEY_CHECKS = 1;
基于MyBatis的多數(shù)據(jù)源實現(xiàn)
首先創(chuàng)建一個MyBatis項目目派,項目結(jié)構(gòu)如下:
這里有一點需要注意, StudentMapper
接口和 TeacherMapper
接口是分開的胁赢,它們位于不同子目錄下企蹭,這個后面會提到。
數(shù)據(jù)庫連接配置
既然是多數(shù)據(jù)源智末,數(shù)據(jù)庫連接的信息就有可能存在不同谅摄,所以需要在配置文件中配置各個數(shù)據(jù)源的連接信息(這里使用了druid數(shù)據(jù)庫連接池)。
spring:
datasource:
ds1: #數(shù)據(jù)源1系馆,默認(rèn)數(shù)據(jù)源
url: jdbc:mysql://localhost:3306/ds1?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
username: root
password: root
typ: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
filters: stat
maxActive: 2
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
ds2: #數(shù)據(jù)源2
url: jdbc:mysql://localhost:3306/ds2?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=utf8
username: root
password: root
typ: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
filters: stat
maxActive: 2
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxOpenPreparedStatements: 20
注意不同的數(shù)據(jù)源要用不同的屬性名區(qū)分送漠。
重寫SpringBoot的數(shù)據(jù)源配置
1、數(shù)據(jù)源1的配置
@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds1"}, sqlSessionFactoryRef = "sqlSessionFactory1")
public class Datasource1Configuration {
@Value("${mybatis.mapper-locations}")
private String mapperLocation;
@Value("${spring.datasource.ds1.url}")
private String jdbcUrl;
@Value("${spring.datasource.ds1.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.ds1.username}")
private String username;
@Value("${spring.datasource.ds1.password}")
private String password;
@Value("${spring.datasource.ds1.initialSize}")
private int initialSize;
@Value("${spring.datasource.ds1.minIdle}")
private int minIdle;
@Value("${spring.datasource.ds1.maxActive}")
private int maxActive;
@Bean(name = "dataSource1")
@Primary
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(jdbcUrl);
dataSource.setDriverClassName(driverClassName);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
return dataSource;
}
@Bean("sqlSessionFactory1")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
@Bean("sqlSessionTemplate1")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory1") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean("transactionManager1")
public DataSourceTransactionManager transactionManager(@Qualifier("dataSource1")DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
2它呀、數(shù)據(jù)源2的配置
@Configuration
@MapperScan(basePackages = {"com.chou.easyspringboot.multipledatasource.mapper.ds2"}, sqlSessionFactoryRef = "sqlSessionFactory2")
public class Datasource2Configuration {
@Value("${mybatis.mapper-locations}")
private String mapperLocation;
@Value("${spring.datasource.ds2.url}")
private String jdbcUrl;
@Value("${spring.datasource.ds2.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.ds2.username}")
private String username;
@Value("${spring.datasource.ds2.password}")
private String password;
@Value("${spring.datasource.ds2.initialSize}")
private int initialSize;
@Value("${spring.datasource.ds2.minIdle}")
private int minIdle;
@Value("${spring.datasource.ds2.maxActive}")
private int maxActive;
@Bean(name = "dataSource2")
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(jdbcUrl);
dataSource.setDriverClassName(driverClassName);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
return dataSource;
}
@Bean("sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
@Bean("sqlSessionTemplate2")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory2") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean("transactionManager2")
public DataSourceTransactionManager transactionManager(@Qualifier("dataSource2") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
這里和單數(shù)據(jù)源不同的地方在于對 dataSource
螺男、 sqlSessionFactory
、 sqlSessionTemplate
纵穿、 transactionManager
都進(jìn)行了單獨的配置下隧。另外,數(shù)據(jù)源1和數(shù)據(jù)源2主要存在兩點不同:
@MapperScan
中的包掃描路徑不一樣谓媒,數(shù)據(jù)源1只掃描com.chou.easyspringboot.multipledatasource.mapper.ds1
路徑下的Mapper
淆院,數(shù)據(jù)源2負(fù)責(zé)com.chou.easyspringboot.multipledatasource.mapper.ds2下Mapper
,所以在前面創(chuàng)建的時候我們要把StudentMapper
和TeacherMapper
分開句惯。因為在這里已經(jīng)配置了@MapperScan
土辩,所以在啟動類中必須不能在存在@MapperScan
注解數(shù)據(jù)源1中多一個
@Primary
注解,這是告訴Spring我們使用的默認(rèn)數(shù)據(jù)源抢野,也是多數(shù)據(jù)源項目中必不可少的拷淘。
測試
編寫相應(yīng)的Controller和Service層代碼,查詢所有的Student和Teacher信息指孤,并使用postman模擬發(fā)送請求启涯,會有如下的運行結(jié)果:
-
查詢所有的Student
-
查詢所有Teacher
我們連續(xù)發(fā)送兩個不同的請求贬堵,都得出了想要的結(jié)果,說明MyBatis自動幫我們切換到了對應(yīng)的數(shù)據(jù)源上结洼。
基于自定義注解實現(xiàn)多數(shù)據(jù)源
上面我們提高到數(shù)據(jù)源自動切換主要依靠MyBatis黎做,如果項目中沒有使用MyBatis該如何做呢?
多數(shù)據(jù)源自動切換原理
這里介紹一種基于自定義注解的方法實現(xiàn)多數(shù)據(jù)源的動態(tài)切換松忍。SpringBoot中有一個 AbstractRoutingDataSource
抽象類蒸殿,我們可以實現(xiàn)其抽象方法 determineCurrentLookupKey()
去指定數(shù)據(jù)源。并通過AOP編寫自定義注解處理類鸣峭,在sql語句執(zhí)行前宏所,切換到自定義注解中設(shè)置的數(shù)據(jù)源以實現(xiàn)數(shù)據(jù)源的自動切換。
數(shù)據(jù)庫連接配置
同上配置兩個數(shù)據(jù)庫連接信息叽掘。
創(chuàng)建數(shù)據(jù)源存放類
DataSource
是和線程綁在一起的楣铁,因此,我們需要一個線程安全的類來存放 DataSource
更扁,在determineCurrentLookupKey()
中通過該類獲取數(shù)據(jù)源盖腕。
AbstractRoutingDataSource
類中, DataSource
以鍵值對的形式保存浓镜,可以使用 ThreadLocal
來保存key溃列,從而實現(xiàn)多數(shù)據(jù)源的自動切換。
public class DataSourceContextHolder {
private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
// 使用ThreadLocal線程安全的使用變量副本
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();
/**
* 設(shè)置數(shù)據(jù)源
* */
public static void setDataSource(String dataSource) {
logger.info("切換到數(shù)據(jù)源:{}", dataSource);
CONTEXT_HOLDER.set(dataSource);
}
/**
* 獲取數(shù)據(jù)源
* */
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
/**
* 清空數(shù)據(jù)源
* */
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
數(shù)據(jù)源持有類定義了三個方法膛薛,分別用于數(shù)據(jù)源的設(shè)置听隐、獲取和清除。
創(chuàng)建數(shù)據(jù)源枚舉類
public enum DataSourceEnum {
PRIMARY, //默認(rèn)數(shù)據(jù)源
DATASOURCE1
}
實現(xiàn) determineCurrentLookupKey 方法指定數(shù)據(jù)源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
配置數(shù)據(jù)源
@Configuration
public class DynamicDataSourceConfiguration {
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.ds1")
public DataSource primaryDataSource(){
return new DruidDataSource();
}
@Bean(name = "dataSource1")
@ConfigurationProperties(prefix = "spring.datasource.ds2")
public DataSource dataSource1(){
return new DruidDataSource();
}
@Bean("dynamicDataSource")
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//配置默認(rèn)數(shù)據(jù)源
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
//配置多數(shù)據(jù)源
HashMap<Object, Object> dataSourceMap = new HashMap();
dataSourceMap.put(DataSourceEnum.PRIMARY.name(),primaryDataSource());
dataSourceMap.put(DataSourceEnum.DATASOURCE1.name(),dataSource1());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}
自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceEnum value() default DataSourceEnum.PRIMARY;
}
自定義注解指定作用于方法上并在運行期生效(可以在網(wǎng)上查下如何自定義注解哄啄,這里不在講述)雅任。
AOP攔截
通過AOP在執(zhí)行sql語句前攔截,并切換到自定義注解指定的數(shù)據(jù)源上咨跌。有一點需要注意沪么,自定義數(shù)據(jù)源注解與 @Transaction
注解同一個方法時會先執(zhí)行 @Transaction
,即獲取數(shù)據(jù)源在切換數(shù)據(jù)源之前锌半,所以會導(dǎo)致自定義注解失效禽车,因此需要使用 @Order
(@Order的value越小,就越先執(zhí)行)刊殉,保證該AOP在 @Transactional
之前執(zhí)行殉摔。
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
@Pointcut("@annotation(com.chou.easyspringboot.multipledatasource.annotation.DataSource)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object dataSourceArround(ProceedingJoinPoint proceed) throws Throwable {
MethodSignature methodSignature = (MethodSignature) proceed.getSignature();
Method method = methodSignature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if(dataSource != null) {
DataSourceContextHolder.setDataSource(dataSource.value().name());
}
try {
return proceed.proceed();
} finally {
// 方法執(zhí)行后銷毀數(shù)據(jù)源
DataSourceContextHolder.clearDataSource();
}
}
}
創(chuàng)建啟動類,編寫Controller记焊、Service層代碼
需要在啟動類的 @SpringBootApplication
注解中移除DataSource自動配置類逸月,否則會默認(rèn)自動配置,而不會使用我們自定義的DataSource遍膜,并且啟動會有循環(huán)依賴的錯誤彻采。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class EasyspringbootMultipledatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(EasyspringbootMultipledatasourceApplication.class, args);
}
}
測試
-
查詢所有Student
-
查詢所有Teacher
我們得到了正確的結(jié)果腐缤,數(shù)據(jù)源自動切換了。