解决思路:使用spring提供的AbstractRoutingDataSource结合AOP进行动态配置,ThreadLocal进行动态数据存储。
实现步骤:
- 枚举类 DataSourceType:
枚举多种数据源,与自定义注解配合使用 - 自定义注解 DataSource:
注解,配合AOP可进行无侵入的多数据源切换 - 数据源切换处理 DynamicDataSourceContextHolder:
维护了ThreadLocal对象,用于处理数据源切换 - 多数据源配置 DynamicDataSource(核心):
继承AbstractRoutingDataSource,Spring实现,详见解析 - AOP切面 DataSourceAspect:
配合注解实现无侵入的动态数据源切换 - 多数据源配置 DruidConfiguration
注入多数据源配置对象
ps:详见源码
使用方式
//在需要更改数据源的方法上加
@DataSource(value = DataSourceType.Slave)
运行原理
-
多数据源初始化
- application.yml中配置多数据源
spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver master: username: root password: 150512 jdbc-url: jdbc:mysql://localhost:3306/cy?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true hikari: minimum-idle: 5 maximum-pool-size: 15 auto-commit: true idle-timeout: 30000 max-lifetime: 1800000 connection-timeout: 30000 slave: enabled: true username: root password: 150512 jdbc-url: jdbc:mysql://localhost:3306/yc?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true hikari: minimum-idle: 5 maximum-pool-size: 15 auto-commit: true idle-timeout: 30000 max-lifetime: 1800000 connection-timeout: 30000
- 将数据源配置从配置文件中读出,放入targetDataSources这个map中,注入ioc容器
@Bean(name = "dynamicDataSource") @Primary public DynamicDataSource dataSource(){ Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource()); targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource()); return new DynamicDataSource(masterDataSource(), targetDataSources); }
- 初始化动态数据源
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){ super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); }
-
在需要切换数据源的方法上添加注解
@GetMapping("/testDs") @SwitchDataSource(value = DataSourceType.SLAVE) public Object testDs(){ String sql="select id,username from user where id=?"; RowMapper<User> rowMapper=new BeanPropertyRowMapper<>(User.class); User user = jdbcTemplate.queryForObject(sql, rowMapper,52); return user; }
-
系统检测到注解,执行AOP方法,切换数据源
@Pointcut(value = "@annotation(com.cy.freesql.datasource.SwitchDataSource)") public void dsPointCut(){}
- 通过连接点获取注解
@Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable{ MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class); if (null != switchDataSource) { DynamicDataSourceContextHolder.setDateSourceType(switchDataSource.value().name()); } try { return point.proceed(); }finally { //销毁数据源,在执行方法之后 DynamicDataSourceContextHolder.clearDataSourceType(); } }
- 将ThreadLocal设为当前注解中枚举类的取值
/* * 使用ThreadLocal维护变量,ThreadLocal为每个使用变量的线程提供独立的副本 * 所以每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本 * */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); //设置数据源 public static void setDateSourceType(String dsType){ log.info("切换到{}数据源", dsType); CONTEXT_HOLDER.set(dsType); }
- determineCurrentLookupKey()返回该值
/* * 该方法返回需要使用的DataSource的key值 * 然后根据这个key从resolveDataSource这个map里取出对应的DataSource * 若找不到,则用默认的resolvedDefaultDataSource * */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceType(); }
- Spring以该值为key,切换到对应的数据源
在方法执行后,销毁数据源(切换回默认数据源)
Druid与Hikari
- 连接池为Druid时,实现多数据源配置
注意:注入数据源的过程中,DruidDataSourceBuilder只需要指定在bean上@ConfigurationProperties("spring.datasource.druid.master")即可从配置文件中装配 - 连接池为Hikari时,实现多数据源配置
(spring-boot-starter-jdbc默认使用)
注意:Hikara并不能autoconfigure,显式的开启@ConfigurationProperties支持,需要在启动类上加@EnableConfigurationProperties(DataSourceProperties.class)注解
测试
注解
- @ConfigurationProperties注解:
使用@EnableConfigurationProperties开启@ConfigurantionProperties注解的支持。使用该注解的bean可以通过标准方式注册到容器。
@EnableConfigurationProperties只定义了一个value属性,用于设置一组使用了注解的@ConfigurationProperties的类,可以作为bean定义注册到容器中。 - @ConditionOnProperty注解:
控制某个Configuration是否生效,通过name以及havingValue实现,其中name用来从application.yml中读取某个属性,若值为空,则返回false,若值不为空,则将该值与havingValue指定的值进行比较,如果一样返回true,否则返回false,若返回false,则该configuration不生效,true则生效
AbstractRoutingDataSource
Spring提供的动态数据源配置类,充当了DataSource的路由中介,能在运行时,根据某种key值动态切换到真正的DataSource上.
构造函数
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
targetDataSources目标数据源,存放多数据源
defaultTargetDataSource默认数据源,初始化、通过key未寻找到数据源、使用切换后数据源方法结束时会使用该数据源
在DataSourceConfiguration中,调用该构造方法,初始化DynamicDataSource后注入IOC容器
数据源解析
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
将构造函数传入数据源解析后分别存为resolvedDataSources和defaultTargetDataSource
工作机制
-
@Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); }
从determineTargetDataSource()中获取连接
-
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey() DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
从determineCurrentLookupKey()中获取lookupKey,再去resolvedDataSources中根据lookupKey获取dataSource
lenientFallback控制在通过lookupKey无法获取到dataSource时,是否使用默认数据源
-
@Nullable protected abstract Object determineCurrentLookupKey();
抽象方法,由实现类返回一个key