一、spring 多数据源配置常见的两种方案:
1、在spring项目启动的时候直接配置多个不同的数据源,每个数据源配置各自的sessionFactory、事务管理器。指定不同的DAO对应不同的数据源。(一般是根据mapper路径在指定的,比如说指定com.sid.mapper.A 这个包下所有的mapper使用datasourceA数据源,指定com.sid.mapper.B这个包下所有的mapper使用datasourceB数据源)
2、配置多个不同的数据源,使用一个sessionFactory。在业务中自己动态切换数据源。为了保证并发安全,需要使用threadlocal来实现多线程竞争切换数据源的问题。
这里是第二种
二、代码
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sid</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring-boot的web启动的jar包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- mysql数据库连接包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 8088
context-path: /sid
spring:
datasource:
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
druid:
one: #数据源1
url: jdbc:mysql://localhost:3306/sid
username: root
password: root
two: #数据源2
url: jdbc:mysql://localhost:3306/test
username: root
password: root
#初始化时建立物理连接的个数
initialSize: 1
#池中最大连接数
maxActive: 20
#最小空闲连接
minIdle: 1
#获取连接时最大等待时间,单位毫秒
maxWait: 60000
#有两个含义:
#1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
#2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
timeBetweenEvictionRunsMillis: 60000
#连接保持空闲而不被驱逐的最小时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
#使用该SQL语句检查链接是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQuery: SELECT 1 FROM DUAL
#建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
#归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
#connectionProperties.druid.stat.mergeSql: true
#connectionProperties.druid.stat.slowSqlMillis: 5000
# 合并多个DruidDataSource的监控数据
#useGlobalDataSourceStat: true
#default-auto-commit: true 默认
## 该配置节点为独立的节点,不是在在spring的节点下
mybatis:
mapper-locations: classpath:mapping/*.xml #注意:一定要对应mapper映射xml文件的所在路径
type-aliases-package: com.sid.model # 注意:对应实体类的路径
configuration:
#log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #控制台打印sql
定义多个数据源的名字
/**
* @program: springboot
* @description: 数据源名称
* @author: Sid
* @date: 2018-11-22 13:59
* @since: 1.0
**/
public interface DataSourceNames {
String ONE = "ONE";
String TWO = "TWO";
}
扩展AbstractRoutingDataSource类,实现动态数据源
package com.sid.configuration.multi.datasource.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* @program: springboot
* @description: 创建动态数据源
* 实现数据源切换的功能就是自定义一个类扩展AbstractRoutingDataSource抽象类,
* 其实该相当于数据源DataSourcer的路由中介,
* 可以实现在项目运行时根据相应key值切换到对应的数据源DataSource上。
* @author: Sid
* @date: 2018-11-22 13:59
* @since: 1.0
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 配置DataSource, defaultTargetDataSource为主数据库
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
//设置默认数据源
super.setDefaultTargetDataSource(defaultTargetDataSource);
//设置数据源列表
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 是实现数据源切换要扩展的方法,
* 该方法的返回值就是项目中所要用的DataSource的key值,
* 拿到该key后就可以在resolvedDataSource中取出对应的DataSource,
* 如果key找不到对应的DataSource就使用默认的数据源。
* */
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 绑定当前线程数据源路由的key
* 使用完成后必须调用removeRouteKey()方法删除
*/
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
/**
* 获取当前线程的数据源路由的key
*/
public static String getDataSource() {
return contextHolder.get();
}
/**
* 删除与当前线程绑定的数据源路由的key
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
配置多个数据源和动态数据源
package com.sid.configuration.multi.datasource.dynamic;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @program: springboot
* @description: 动态数据源配置
* @author: Sid
* @date: 2018-11-22 14:01
* @since: 1.0
**/
@Configuration
public class DynamicDataSourceConfig {
/**
* 创建 ChangeDataSource Bean
* */
@Bean
@ConfigurationProperties("spring.datasource.druid.one")
public DataSource oneDataSource(){
DataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Bean
@ConfigurationProperties("spring.datasource.druid.two")
public DataSource twoDataSource(){
DataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
/**
* 如果还有数据源,在这继续添加 ChangeDataSource Bean
* */
@Bean
@Primary
public DynamicDataSource dataSource(DataSource oneDataSource, DataSource twoDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DataSourceNames.ONE, oneDataSource);
targetDataSources.put(DataSourceNames.TWO, twoDataSource);
// 还有数据源,在targetDataSources中继续添加
System.out.println("DataSources:" + targetDataSources);
//默认的数据源是oneDataSource
return new DynamicDataSource(oneDataSource, targetDataSources);
}
}
到此动态数据源配置完成,不需要给不同的数据源分别写DAO层,使用的时候切换数据源的方式:
切换到TOW这个数据源
DynamicDataSource.setDataSource(DataSourceNames.TWO);//todo 增删改查
userMapper.insert(user);
使用完后要清空,这样就会回到默认数据源
DynamicDataSource.clearDataSource();
由于每次做数据库增删该查的时候我们都要把逻辑嵌套在切换数据源和恢复默认数据源的代码之间,这里可以做一个AOP,使用注解的方式切换数据源,更方便使用
声明注解
package com.sid.configuration.multi.datasource.dynamic;
import java.lang.annotation.*;
/**
* @program: springboot
* @description: 动态切换数据源注解
* @author: Sid
* @date: 2018-11-22 14:02
* @since: 1.0
**/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ChangeDataSource {
String value() default DataSourceNames.ONE;
}
动态切换数据源AOP切面处理
package com.sid.configuration.multi.datasource.dynamic;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @program: springboot
* @description: 动态切换数据源AOP切面处理
* @author: Sid
* @date: 2018-11-22 14:03
* @since: 1.0
**/
@Aspect
@Component
public class DataSourceAspect implements Ordered {
protected Logger logger = LoggerFactory.getLogger(getClass());
/**
* 切点: 所有配置 ChangeDataSource 注解的方法
*/
@Pointcut("@annotation(com.sid.configuration.multi.datasource.dynamic.ChangeDataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
ChangeDataSource ds = method.getAnnotation(ChangeDataSource.class);
// 通过判断 @ChangeDataSource注解 中的值来判断当前方法应用哪个数据源
DynamicDataSource.setDataSource(ds.value());
System.out.println("当前数据源: " + ds.value());
logger.debug("set datasource is " + ds.value());
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
logger.debug("clean datasource");
}
}
@Override
public int getOrder() {
return 1;
}
}
使用示例
其中addUserA没有@ChangeDataSource注解则代表使用默认的DataSource
package com.sid.service.impl;
import com.sid.configuration.multi.datasource.dynamic.ChangeDataSource;
import com.sid.configuration.multi.datasource.dynamic.DataSourceNames;
import com.sid.mapper.UserMapper;
import com.sid.model.User;
import com.sid.service.MultiDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @program: springboot
* @description:
* @author: Sid
* @date: 2018-11-22 14:20
* @since: 1.0
**/
@Service
public class MultiDataSourceImpl implements MultiDataSource {
@Autowired
private UserMapper userMapper;
@Override
public User addUserA(User user) {
userMapper.insert(user);
return user;
}
@ChangeDataSource(DataSourceNames.TWO)
@Override
public User addUserB(User user) {
userMapper.insert(user);
return user;
}
}