问题引入
在大型分布式项目中,经常会出现多数据源的情况,比如说mysql结合sqlServer、Oracle等进行数据存储,此时就需要我们通过Spring配值多数据源,在项目中根据实际进行动态切换,按相应的数据库进行CRUD操作。
难点所在
由于项目中的Bean基本都为单例模式,此时如果大量用户不断切换数据库,改变dataSource,会造成严重的资源掠夺问题,显然,此时解决方案有两个思路,一是:以空间换取时间,二是:以时间换区空间。
首先第一种解决方法很简单,在修改dataSource上加入Synchronized,但在高并发环境下是完全不合理的,很影响性能;
而第二种解决方法较为灵活,以“空间换时间”,为每一个线程提供一份变量(利用ThreadLocal),因此可以同时访问并互不干扰。
废话不多说,直接看实现方法!
具体方法
创建SpringBoot项目,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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.capol</groupId>
<artifactId>multidatasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>multidatasource</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</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在application.yml中进行如下配置
spring:
datasource:
sqlserver:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc-url:
username:
password:
mysql:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url:
username:
password:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
首先定义一个数据库的枚举类
package com.capol.multidatasource.config;
/**
* @author fuzihao
* @date 2019/7/26 16:08
*/
public enum DataSourceEnum {
MYSQL_DATASOURCE,
SQLSERVER_DATASOURCE
}
其次,有如下的类,持有数据库连接对象与线程中,需要使用ThreadLocal
package com.capol.multidatasource.config;
/**
* @author fuzihao
* @date 2019/7/26 15:38
*/
public class DataSourceContextHolder {
/**
* 通过ThreadLocal保证线程安全
*/
private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();
/**
* 设置数据源变量
* @param dataSourceEnum 数据源变量
*/
public static void setDataBaseType(DataSourceEnum dataSourceEnum) {
System.out.println("修改数据源为:" + dataSourceEnum);
contextHolder.set(dataSourceEnum);
}
/**
* 获取数据源变量
* @return 数据源变量
*/
public static DataSourceEnum getDataBaseType() {
DataSourceEnum dataSourceEnum = contextHolder.get() == null ? DataSourceEnum.MYSQL_DATASOURCE : contextHolder.get();
System.out.println("当前数据源的类型为:" + dataSourceEnum);
return dataSourceEnum;
}
/**
* 清空数据类型
*/
public static void clearDataBaseType() {
contextHolder.remove();
}
}
同时sqlSessionFactory需要一个DynamicDataSource,其继承至AbstractRoutingDatasource,如下
package com.capol.multidatasource.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author fuzihao
* @date 2019/7/26 14:58
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataBaseType();
}
}
此时就可以注入各Bean了
package com.capol.multidatasource.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author fuzihao
* @date 2019/7/26 14:43
*/
@Configuration
public class DataSourceConfig {
@Bean(name = "sqlserverDataSource")
@ConfigurationProperties(prefix = "spring.datasource.sqlserver")
public DataSource getDateSource1() {
return DataSourceBuilder.create().build();
}
@Bean(name = "mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource getDateSource2() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
public DynamicDataSource DataSource(@Qualifier("sqlserverDataSource") DataSource sqlserverDataSource,
@Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
//配置多数据源
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceEnum.SQLSERVER_DATASOURCE, sqlserverDataSource);
targetDataSource.put(DataSourceEnum.MYSQL_DATASOURCE, mysqlDataSource);
DynamicDataSource dataSource = new DynamicDataSource();
//多数据源
dataSource.setTargetDataSources(targetDataSource);
//默认数据源
dataSource.setDefaultTargetDataSource(mysqlDataSource);
return dataSource;
}
@Bean(name = "SqlSessionFactory")
public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
return bean.getObject();
}
}
由于mysql数据库中有一个t_blog表,故我在这里简单写了下mapper和entity:
package com.capol.multidatasource.mapper;
import com.capol.multidatasource.entity.Blog;
import org.apache.ibatis.annotations.Select;
/**
* @author fuzihao
* @date 2019/7/26 15:47
*/
public interface UserMapper {
@Select("select pk_t_blog as id,f_author as author from t_blog where pk_t_blog=#{id}")
Blog selectBlog(Integer id);
}
package com.capol.multidatasource.entity;
/**
* @author fuzihao
* @date 2019/7/26 15:46
*/
public class Blog {
private Integer id;
private String author;
//getter and setter
}
此时就也可以在测试类中进行测试了:
package com.capol.multidatasource;
import com.capol.multidatasource.config.DataSourceContextHolder;
import com.capol.multidatasource.config.DataSourceEnum;
import com.capol.multidatasource.entity.Blog;
import com.capol.multidatasource.mapper.UserMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@MapperScan(basePackages = "com.capol.multidatasource.mapper")
public class MultidatasourceApplicationTests {
@Autowired
@Qualifier("SqlSessionFactory")
private SqlSessionFactory sqlSessionFactory;
@Autowired
private UserMapper userMapper;
@Test
public void contextLoads() {
Blog blog = userMapper.selectBlog(1);
System.out.println(blog);
}
}
此时输出实体类对象,若在config中设置默认为sqlServer数据库,或者在测试中插入
DataSourceContextHolder.setDataBaseType(DataSourceEnum.SQLSERVER_DATASOURCE);
发现会报错找不到 t_blog 表,配置成功!
知识扩展1
为了方便我们在Mapper中切换各数据库,可以通过注解和AOP的方式来实现数据源的切换。
首先定义一个注解类:
package com.capol.multidatasource.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义数据源注解,默认为mysql数据源
* @author fuzihao
* @date 2019/7/26 16:26
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSourceAnno {
DataSourceEnum value() default DataSourceEnum.MYSQL_DATASOURCE;
}
其次,给注解切面
package com.capol.multidatasource.config;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author fuzihao
* @date 2019/7/26 16:27
*/
@Aspect
@Component
public class DynamicDataSourceAspect {
@Around("@annotation(dataSourceAnno)")
public Object aroudOperDeal(ProceedingJoinPoint point,DataSourceAnno dataSourceAnno) throws Throwable {
DataSourceContextHolder.setDataBaseType(dataSourceAnno.value());
// 让目标方法继续进行
Object retVal=point.proceed();
DataSourceContextHolder.clearDataBaseType();
return retVal;
}
}
此时,我们就可以在mapper中,在相应数据库的curd操作上加上此注解,修改对应数据源啦!
知识扩展2
为什么最后需要contextHolder.remove();
首先看下Thread、ThreadLocalMap、ThreadLocal之间的关系
Thread类有属性变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap (具体表现即为图中的各个Entry),所以每个线程往这个ThreadLocal中读写隔离的,并且是互相不会影响的。
一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!!!
可以看到Entry的key指向ThreadLocal为虚线,即表示弱引用
- 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。
即如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
- 如果线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
解决方法
每次使用完ThreadLocal都调用它的remove()方法清除数据;
- remove作用?
remove方法会把这个key对应Entry的值设为空
或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
所以,最佳实践做法应该为:
public class XXX{
private static ThreadLocal<Integer> threadlocal=new ThreadLocal<>();
try{
...
}
finally{
threadlocal.remove();
}
}
这也解释了为什么我们需要在最后进行contextHolder.remove()。