Spring Boot 中的多数据源配置方案
多数据源可以理解为多数据库,甚至可以是多个不同类型的数据库,比如一个是MySql,一个是Oracle。随着项目的扩大,有时需要数据库的拆分或者引入另一个数据库,这时就需要配置多个数据源。
SpringBoot中使用多数据源还是比较简单的,为了演示方便,我们在MySql中创建两个数据库:ds1、ds2,并在ds1数据库中创建student表,在ds2数据库中创建teacher表。数据库脚本如下:
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的多数据源实现
首先创建一个MyBatis项目,项目结构如下:
image这里有一点需要注意, StudentMapper
接口和 TeacherMapper
接口是分开的,它们位于不同子目录下,这个后面会提到。
数据库连接配置
既然是多数据源,数据库连接的信息就有可能存在不同,所以需要在配置文件中配置各个数据源的连接信息(这里使用了druid数据库连接池)。
spring:
datasource:
ds1: #数据源1,默认数据源
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: #数据源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
注意不同的数据源要用不同的属性名区分。
重写SpringBoot的数据源配置
1、数据源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、数据源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);
}
}
这里和单数据源不同的地方在于对 dataSource
、 sqlSessionFactory
、 sqlSessionTemplate
、 transactionManager
都进行了单独的配置。另外,数据源1和数据源2主要存在两点不同:
-
@MapperScan
中的包扫描路径不一样,数据源1只扫描com.chou.easyspringboot.multipledatasource.mapper.ds1
路径下的Mapper
,数据源2负责com.chou.easyspringboot.multipledatasource.mapper.ds2下Mapper
,所以在前面创建的时候我们要把StudentMapper
和TeacherMapper
分开。因为在这里已经配置了@MapperScan
,所以在启动类中必须不能在存在@MapperScan
注解 -
数据源1中多一个
@Primary
注解,这是告诉Spring我们使用的默认数据源,也是多数据源项目中必不可少的。
测试
编写相应的Controller和Service层代码,查询所有的Student和Teacher信息,并使用postman模拟发送请求,会有如下的运行结果:
-
查询所有的Student
image -
查询所有Teacher
image
我们连续发送两个不同的请求,都得出了想要的结果,说明MyBatis自动帮我们切换到了对应的数据源上。
基于自定义注解实现多数据源
上面我们提高到数据源自动切换主要依靠MyBatis,如果项目中没有使用MyBatis该如何做呢?
多数据源自动切换原理
这里介绍一种基于自定义注解的方法实现多数据源的动态切换。SpringBoot中有一个 AbstractRoutingDataSource
抽象类,我们可以实现其抽象方法 determineCurrentLookupKey()
去指定数据源。并通过AOP编写自定义注解处理类,在sql语句执行前,切换到自定义注解中设置的数据源以实现数据源的自动切换。
数据库连接配置
同上配置两个数据库连接信息。
创建数据源存放类
DataSource
是和线程绑在一起的,因此,我们需要一个线程安全的类来存放 DataSource
,在determineCurrentLookupKey()
中通过该类获取数据源。
AbstractRoutingDataSource
类中, DataSource
以键值对的形式保存,可以使用 ThreadLocal
来保存key,从而实现多数据源的自动切换。
public class DataSourceContextHolder {
private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
// 使用ThreadLocal线程安全的使用变量副本
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();
/**
* 设置数据源
* */
public static void setDataSource(String dataSource) {
logger.info("切换到数据源:{}", dataSource);
CONTEXT_HOLDER.set(dataSource);
}
/**
* 获取数据源
* */
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源
* */
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
数据源持有类定义了三个方法,分别用于数据源的设置、获取和清除。
创建数据源枚举类
public enum DataSourceEnum {
PRIMARY, //默认数据源
DATASOURCE1
}
实现 determineCurrentLookupKey 方法指定数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
配置数据源
@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();
//配置默认数据源
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
//配置多数据源
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;
}
自定义注解指定作用于方法上并在运行期生效(可以在网上查下如何自定义注解,这里不在讲述)。
AOP拦截
通过AOP在执行sql语句前拦截,并切换到自定义注解指定的数据源上。有一点需要注意,自定义数据源注解与 @Transaction
注解同一个方法时会先执行 @Transaction
,即获取数据源在切换数据源之前,所以会导致自定义注解失效,因此需要使用 @Order
(@Order的value越小,就越先执行),保证该AOP在 @Transactional
之前执行。
@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 {
// 方法执行后销毁数据源
DataSourceContextHolder.clearDataSource();
}
}
}
创建启动类,编写Controller、Service层代码
需要在启动类的 @SpringBootApplication
注解中移除DataSource自动配置类,否则会默认自动配置,而不会使用我们自定义的DataSource,并且启动会有循环依赖的错误。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class EasyspringbootMultipledatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(EasyspringbootMultipledatasourceApplication.class, args);
}
}
测试
-
查询所有Student
image -
查询所有Teacher
image
我们得到了正确的结果,数据源自动切换了。