- 原理概述
- 具体实现
- 环境搭建
- 数据源配置
- 在配置文件中配置数据库的连接信息
- 实现AbstractRoutingDataSource抽象类
- 替换mybaits中的sqlSessionFactory,以及事务管理器
- 注解及切面的实现
- 效果展示
- 问题总结
- 参考来源
主要设计三大部分:druid连接池、spring内置的抽象类AbstractRoutingDataSource、注解及切面的配合使用。其中担任动态数据源切换重要角色的是AbstractRoutingDataSource的实现类,通过他我们可以设置默认的数据源以及多个待选数据源。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map
详细用法请接着往下看
具体实现 环境搭建首先添加druid、mybatis等pom依赖、保证项目里有这些依赖就可以,其他基础依赖此处省略
com.alibaba
druid-spring-boot-starter
1.1.21
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.2
org.springframework.boot
spring-boot-starter-aop
mysql
mysql-connector-java
8.0.11
com.alibaba
fastjson
1.2.70
数据源配置
在配置文件中配置数据库的连接信息
#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 {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean(name = "dataSource")
@Primary
public DataSource masterDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
@ConfigurationProperties(prefix = "spring.datasource.red12hours")
@Bean(name = "redDataSource")
public DataSource redDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
@ConfigurationProperties(prefix = "spring.datasource.publicweb")
@Bean(name = "publicwebDataSource")
public DataSource publicwebDataSource(){
//做一些其他的自定义配置,比如密码加密等......
return new DruidDataSource();
}
}
三个数据源则需要创建三个Bean对象,在@ConfigurationProperties注解中写好需要用的配置的前缀即可当做参数注入到新建的对象中,需要保证yml文件配置名和参数名的一致,写到这里还不算完,这个配置类后续还需要加入其他的配置Bean。
实现AbstractRoutingDataSource抽象类因为druid连接池是公共的资源,不能因为这里的切换而影响了其他线程的执行,所以引入ThreadLocal来保证线程间的隔离
public class DataSourceHolder {
//线程 本地环境
private static final ThreadLocal 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类了
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
//从ThreadLocal中取出KEY
return DataSourceHolder.getDataSource();
}
public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) {
//默认的数据源,可以作为主数据源
super.setDefaultTargetDataSource(defaultTargetDataSource);
//目标数据源
super.setTargetDataSources(targetDataSources);
//执行afterPropertiesSet方法,完成属性的设置
super.afterPropertiesSet();
}
}
下一步,创建DynamicDataSourceBean,将多数据源注入其中,在上面编写的DataSourcesConfig配置类中加入下面的代码
@Bean(name = "dynamicDataSource")
public DynamicDataSource DynamicDataSource(
@Autowired @Qualifier("dataSource") DataSource primary,
@Autowired @Qualifier("publicwebDataSource") DataSource publicwebDataSource,
@Autowired @Qualifier("redDataSource") DataSource redDataSource
){
HashMap ds = new HashMap<>();
ds.put("redDataSource",redDataSource);
ds.put("publicwebDataSource",publicwebDataSource);
//做一些其他的自定义配置,比如密码加密等......
DynamicDataSource dynamicDataSource = new DynamicDataSource(primary,ds);
return dynamicDataSource;
}
替换mybaits中的sqlSessionFactory,以及事务管理器
注意入参是我们自己定义的实现了AbstractRoutingDataSource 类的多数据源管理对象
@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 ListgetAllData(){ ArrayList jsonObjects = new ArrayList<>(); //不指定数据源则使用默认的 List sysUser = dsTestMapper.getSysUserList(); DataSourceHolder.setDataSource("redDataSource"); //切换数据源 List 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 {
String DEFAULT_NAME = "redDataSource";
String value() default DEFAULT_NAME;
}
对加注解了的方法添加环绕切面
@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {
@Pointcut("@annotation(SwitchSource)")
public void pointcut() {
}
@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());
}
@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 getSysUser(){
List sysUser = dsTestMapper.getSysUserList();
return sysUser;
}
@SwitchSource("redDataSource")
@RequestMapping(value = "getWxUser",method = RequestMethod.GET)
@Transactional(rollbackFor = Exception.class)
public List getWxUser(){
List sysUser = dsTestMapper.getWxUser();
return sysUser;
}
@SwitchSource("publicwebDataSource")
@RequestMapping(value = "getPublicDoc",method = RequestMethod.GET)
@Transactional(rollbackFor = Exception.class)
public List getPublicDoc(){
List sysUser = dsTestMapper.getPublicDoc();
return sysUser;
}
问题总结
需要注意的一点是因为切换数据源是通过注解加切面实现的,所以就会出现以下问题:
A类中有m方法使用m数据源,n方法使用n数据源,同时在A类中有方法k来调用m,n方法,此时发现切换数据源是不生效的,都是使用的k方法的数据源,解决方法就是把k方法放到m和n所在类以外的其他类中,此类问题在使用注解加切面的编程中都会存在,例如SpringCache的使用。
微信公众号:程序员DD,干货很多,此篇大多参考 link这边文章,并经过实践后,总结的使用方法,里面有很多原理、源码,可以详读。但是原博文里面缺少了一部分代码,就是创建DynamicDataSourceBean的那部分,没有那部分会导致项目启动报错



