springboot实现方法级别的动态多数据源切换
原理概述
主要设计三大部分:druid连接池、spring内置的抽象类AbstractRoutingDataSource
、注解及切面的配合使用。其中担任动态数据源切换重要角色的是AbstractRoutingDataSource
的实现类,通过他我们可以设置默认的数据源以及多个待选数据源。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources; //目标数据源的map,key是数据源的名称,value是配置的DataSource对象
@Nullable
private Object defaultTargetDataSource; //默认数据源
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
...
...
详细用法请接着往下看
具体实现
环境搭建
首先添加druid、mybatis等pom依赖、保证项目里有这些依赖就可以,其他基础依赖此处省略
<!--druid数据连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
数据源配置
在配置文件中配置数据库的连接信息
#mysql数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
filters: mergeStat
initial-size: 0
log-abandoned: true
max-active: 20
max-wait: 6000
min-evictable-idle-time-millis: 25200000
min-idle: 0
remove-abandoned-timeout: 1800
removeAbandoned: true
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
validation-query: SELECT 1
password: xxxx
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://xxxx:3306/yuhang?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: xxxx
red12hours:
password: xxxx
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://xx.xx.xx.xx:3306/red_12_hours?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: xxxx
driverClassName: com.mysql.cj.jdbc.Driver
publicweb:
password: xxxxx
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://xx.xx.xx.xx:3306/publicweb?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: xxxx
driverClassName: com.mysql.cj.jdbc.Driver
此处共配置了三个数据源,以及其他的一些druid连接池的配置,写好yml配置文件后,需要编写springboot的Configuration
类来创建对应的Bean存入IOC容器中。
@Configuration
public class DataSourcesConfig {
/**
* @Bean:向IOC容器中注入一个Bean
* @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
* @Primary注解很重要,因为系统中存在多个数据源,需要指定一个主要的,否则启动项目会报错,当然只需要加一个就可以,其他数据源的配置不需要加
* @return
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean(name = "dataSource")
@Primary
public DataSource masterDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
/**
* @Bean:向IOC容器中注入一个Bean
* @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
* @return
*/
@ConfigurationProperties(prefix = "spring.datasource.red12hours")
@Bean(name = "redDataSource")
public DataSource redDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
/**
* @Bean:向IOC容器中注入一个Bean
* @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
* @return
*/
@ConfigurationProperties(prefix = "spring.datasource.publicweb")
@Bean(name = "publicwebDataSource")
public DataSource publicwebDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
}
三个数据源则需要创建三个Bean对象,在@ConfigurationProperties
注解中写好需要用的配置的前缀即可当做参数注入到新建的对象中,需要保证yml文件配置名和参数名的一致,写到这里还不算完,这个配置类后续还需要加入其他的配置Bean。
实现AbstractRoutingDataSource抽象类
因为druid连接池是公共的资源,不能因为这里的切换而影响了其他线程的执行,所以引入ThreadLocal来保证线程间的隔离
/**
* 使用ThreadLocal存储切换数据源后的KEY
*/
public class DataSourceHolder {
//线程 本地环境
private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();
//设置数据源
public static void setDataSource(String datasource) {
dataSources.set(datasource);
}
//获取数据源
public static String getDataSource() {
return dataSources.get();
}
//清除数据源
public static void clearDataSource() {
dataSources.remove();
}
}
因为ThreadLocal
可以实现线程与线程之间的隔离,所以用ThreadLocal
存储当前数据源的名称,每个线程都有其自己的当前数据源,所以单线程的切换不会影响到其他的线程,下面就要实现AbstractRoutingDataSource
类了
/**
* 动态数据源,继承AbstractRoutingDataSource
* 这里不需要通过@Compent注解来注入IOC容器,需要调用构造器手动创建Bean
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 返回需要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换)
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
//从ThreadLocal中取出KEY
return DataSourceHolder.getDataSource();
}
/**
* 构造方法填充Map,构建多数据源
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
//默认的数据源,可以作为主数据源
super.setDefaultTargetDataSource(defaultTargetDataSource);
//目标数据源
super.setTargetDataSources(targetDataSources);
//执行afterPropertiesSet方法,完成属性的设置
super.afterPropertiesSet();
}
}
下一步,创建DynamicDataSource
Bean,将多数据源注入其中,在上面编写的DataSourcesConfig
配置类中加入下面的代码
@Bean(name = "dynamicDataSource")
public DynamicDataSource DynamicDataSource(
@Autowired @Qualifier("dataSource") DataSource primary,
@Autowired @Qualifier("publicwebDataSource") DataSource publicwebDataSource,
@Autowired @Qualifier("redDataSource") DataSource redDataSource
){
HashMap<Object, Object> ds = new HashMap<>();
ds.put("redDataSource",redDataSource);
ds.put("publicwebDataSource",publicwebDataSource);
//做一些其他的自定义配置,比如密码加密等......
DynamicDataSource dynamicDataSource = new DynamicDataSource(primary,ds);
return dynamicDataSource;
}
替换mybaits中的sqlSessionFactory,以及事务管理器
注意入参是我们自己定义的实现了AbstractRoutingDataSource
类的多数据源管理对象
/**
* 创建动态数据源的SqlSessionFactory,传入的是动态数据源
* @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
*/
@Primary
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setDefaultFetchSize(100);
configuration.setDefaultStatementTimeout(30);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
/**
* 重写事务管理器,管理动态数据源
*/
@Primary
@Bean(value = "transactionManager")
public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
OK,截止到目前系统中已经具备了多数据源动态切换的能力,你可以 通过以下方式来实现数据源的切换,同时,以下方式可以实现同一方法内去执行不同的数据源查询,但是事务管理此时应该是不生效了,但是还没有实测。
public List<JSONObject> getAllData(){
ArrayList<JSONObject> jsonObjects = new ArrayList<>();
//不指定数据源则使用默认的
List<JSONObject> sysUser = dsTestMapper.getSysUserList();
DataSourceHolder.setDataSource("redDataSource"); //切换数据源
List<JSONObject> sysUser2 = dsTestMapper.getWxUser();
DataSourceHolder.clearDataSource(); //使用完记得清除掉数据源,相当于切换回默认数据源
jsonObjects.add(sysUser.get(0));
jsonObjects.add(sysUser2.get(0));
return jsonObjects;
}
而实际项目中,较多的场景是方法内数据源一致,所以搭配注解来实现方法级别的数据源切换是非常常用的。
注解及切面的实现
编写注解:
/**
* 切换数据源的注解
*/
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {
/**
* 默认切换的数据源KEY
*/
String DEFAULT_NAME = "redDataSource";
/**
* 需要切换到数据的KEY
*/
String value() default DEFAULT_NAME;
}
对加注解了的方法添加环绕切面
@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
@Pointcut("@annotation(SwitchSource)")
public void pointcut() {
}
/**
* 在方法执行之前切换到指定的数据源
* @param joinPoint
*/
@Before(value = "pointcut()")
public void beforeOpt(JoinPoint joinPoint) {
/*因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,将数据源设置成远方,然后结束后,清楚当前线程数据源*/
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
log.info("[Switch DataSource]:" + switchSource.value());
DataSourceHolder.setDataSource(switchSource.value());
}
/**
* 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
*/
@After(value = "pointcut()")
public void afterOpt() {
DataSourceHolder.clearDataSource();
log.info("[Switch Default DataSource]");
}
}
效果展示
@RequestMapping(value = "getSysUser",method = RequestMethod.GET)
@Transactional(rollbackFor = Exception.class)
public List<JSONObject> getSysUser(){
List<JSONObject> sysUser = dsTestMapper.getSysUserList();
return sysUser;
}
@SwitchSource("redDataSource")
@RequestMapping(value = "getWxUser",method = RequestMethod.GET)
@Transactional(rollbackFor = Exception.class)
public List<JSONObject> getWxUser(){
List<JSONObject> sysUser = dsTestMapper.getWxUser();
return sysUser;
}
@SwitchSource("publicwebDataSource")
@RequestMapping(value = "getPublicDoc",method = RequestMethod.GET)
@Transactional(rollbackFor = Exception.class)
public List<JSONObject> getPublicDoc(){
List<JSONObject> sysUser = dsTestMapper.getPublicDoc();
return sysUser;
}
问题总结
需要注意的一点是因为切换数据源是通过注解加切面实现的,所以就会出现以下问题:
A类中有m方法使用m数据源,n方法使用n数据源,同时在A类中有方法k来调用m,n方法,此时发现切换数据源是不生效的,都是使用的k方法的数据源,解决方法就是把k方法放到m和n所在类以外的其他类中,此类问题在使用注解加切面的编程中都会存在,例如SpringCache的使用。
参考来源
微信公众号:程序员DD,干货很多,此篇大多参考 link这边文章,并经过实践后,总结的使用方法,里面有很多原理、源码,可以详读。但是原博文里面缺少了一部分代码,就是创建DynamicDataSource
Bean的那部分,没有那部分会导致项目启动报错