【多数据源】dynamic-datasource原理及使用
多数据源实现
如果只在意实现,直接看 dynamic-datasource【开源组件实现多数据源】🚩
多数据源的需求:
- 不同的业务分多个数据库场景,例如一个程序负责n个省份的db操作
- 一主多从的读写分离的场景(一主多从可以使用myBatis插件的方式实现)
注意:多数据源实现离不开Spring提供的AbstractRoutingDataSource类
AbstractRoutingDataSource类
AbstractRoutingDataSource部分成员变量
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
...
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
...
最终要的三个成员变量🚩
- targetDataSources所有数据源(需指定)
- defaultTargetDataSource默认数据源(需指定)
- resolvedDataSources=targetDataSources(所有数据源最终会赋值到resolvedDataSources)
多数据源执行原理🚩
思路:自定义DynameicDataSource,继承AbstractRoutingDataSource,初始化所有数据源,通过模板方法返回当前数据源标识
-
SpringBoot启动时初始化DynameicDataSource(自定义数据源)的bean对象,此时会调用我们重写的afterPropertiesSet()方法加载所有数据源,为AbstractRoutingDataSource中的targetDataSources 初始化所有数据源,为defaultTargetDataSource 设置默认的数据源。
public class DynameicDataSource extends AbstractRoutingDataSource{ public static ThreadLocal<String> name = new ThreadLocal<>(); @Autowired DataSource dataSource1; @Autowired DataSource dataSource2; //返回当前数据源标识 @Override protected Object determineCurrentLookupKey() { return name.get(); } @Override public void afterPropertiesSet(){ //1.为targetDataSources 初始化所有数据源 HashMap<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("beijing",dataSource1); targetDataSources.put("shanghai",dataSource1); super.setTargetDataSources(targetDataSources); //2.为defaultTargetDataSource 设置默认的数据源 super.setDefaultTargetDataSource(dataSource1); super.afterPropertiesSet(); } }
-
在执行数据库操作时将当前数据源标识设置成对应DB,经过ORM框架一系列Api调用最后会执行到Spring-jdbc的getConnection()方法,根据标识拿到我们提前创建的DataSource(AbstractRoutingDataSource中的getConnection()方法),ORM框架拿到return的数据源后就开始执行对应的数据库操作。
//AbstractRoutingDataSource中部分源码 public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); } ... protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); //获取数据源标识方法,需要子类重写 Object lookupKey = this.determineCurrentLookupKey(); DataSource 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 + "]"); } else { return dataSource; } }
-
通过设置自定义数据源中的标识,手动切换数据源
@RestController @RequestMapping("frend") public class userController{ @Autowired private UserService userService; public List<User> select(){ DynameicDataSource.name.set("beijing"); return userService.list(); } public List<User> insert(){ DynameicDataSource.name.set("shanghai"); userService.save(new User("lihw")); } }
补充:
- @Primary:如果存在多个同类型的bean,使用次注解会优先注入
- implements InitializingBean接口,重写afterPropertiesSet();在Spring容器启动的时候会执行此方法
方案一:使用myBatis插件实现多数据源
适用场景:读写分离
执行原理
- 执行数据库操作时调用MyBatis对应的Api,通过Executor去执行对应的数据库操作
- 在执行数据库操作的方法之前会先执行插件的拦截方法,在拦截方法中设置数据源对应的标识
- 框架调用DataSource.getConnection()时,会来到AbstractRoutingDataSource的getConnection()方法,根据标识获取不同的数据源
- DynameicDataSource中重写过determineCurrentLookupKey()方法,获取当前数据源标识,从resolvedDataSources的Map中根据标识获取对应数据源
Interceptor接口源码:
public interface Interceptor{
//拦截方法
Object intercept(Invocation invocation) throws Throwable;
//返回拦截器的代理对象
Object plugin(Object object);
//设置一些属性
void setProperties(Properties properties);
}
代码实现
插件代理update、query方法,从拦截方法intercept()中获取执行sql的类型,实现数据源的切换。
@Intercepts(
{@Signature(type=Executor.class,method="update",args={MappedStatement.class}),
@Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class)}})
public class DynamicDataSourcePlugin implement Interceptor{
@Override
public Object intercept(Invocation invocation) throws Throwable{
//拿到当前方法(update、query)所有参数
Object[] objects = invocation.getArgs();
//MappedStatement 封装CRUD所有的元素和SQL
MappedStatement ms = (MappedStatement) objects[0];
//修改当前数据源的key
if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
DynamicDataSource.name("R");
}else{
DynamicDataSource.name("W");
}
return invocation.proceed();
}
}
注意:需要将自定义的插件注入到IOC中,mybatis初始化的时候就会加载
public class xxConfig{
@Bean
public Interceptor dynamicDataSourcePlugin(){
return new DynamicDataSourcePlugin();
}
}
方案二:使用AOP+自定义注解的方式实现多数据源
1. pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 自定义注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)//保留方式,可以被JVM加载
public @interface DS{
String value() default "beijing";
}
3. 自定义多个数据源(DynameicDataSource)
初始化beijing、shanghai两个数据源
public class DynameicDataSource extends AbstractRoutingDataSource{
public static ThreadLocal<String> name = new ThreadLocal<>();
@Autowired
DataSource dataSource1;
@Autowired
DataSource dataSource2;
//返回当前数据源标识
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Override
public void afterPropertiesSet(){
//1.为targetDataSources 初始化所有数据源
HashMap<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("beijing",beijingDataSource);
targetDataSources.put("shanghai",shanghaiDataSource);
super.setTargetDataSources(targetDataSources);
//2.为defaultTargetDataSource 设置默认的数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
4. 配置切面类(为ORM指定数据源)
- @Before("within(com.lihw.dynamic.datasource.service.impl.*) && @annotation(DS)") 这个包下所有类有ds注解的都被会动态代理
- 前置通知@Before:为ORM框架指定注解上配置的数据源
@Component
@Aspect
public class DynamicDataSourceAspect{
//前置通知
@Before("within(com.lihw.dynamic.datasource.service.impl.*) && @annotation(DS)")
public void before(JoinPoint joinPoint,DS ds){
//获取注解上的数据源标识
String name = ds.value;
//将标识设置到自定义的数据源上
DynamicDataSource.name.set(name);
}
}
5. 多数据源配置类
package com.lihw.dynameicdatasource.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.beijingDS")
public DataSource dataSource1(){
//底层会自动拿到datasource1中的配置,创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.shanghaiDS")
public DataSource dataSource2(){
//底层会自动拿到datasource2中的配置,创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
6. 配置文件
Spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
beijingDS:
driver-class-name: com.mysql.cj/jdbc/Driver
url: jdbc:mysql://127.0.0.1:3306/testdb1?serverTimezone=UTC
username: root
password: 1234
initial-size: 1
min-idle: 1
max-active: 20
test-on-nborrow: true
shanghaiDS:
driver-class-name: com.mysql.cj/jdbc/Driver
url: jdbc:mysql://127.0.0.1:3306/testdb1?serverTimezone=UTC
username: root
password: 1234
initial-size: 1
min-idle: 1
max-active: 20
test-on-nborrow: true
6. controller/service
-
userController
@RestController @RequestMapping("frend") public class userController{ @Autowired private UserService userService; public List<User> select(){ return userService.list(); } public List<User> insert(){ userService.save(new User("lihw")); } }
-
userService
@Service public class UserServiceImpl implement UserService{ @Autowired private UserMapper userMapper; @Autowired @DS("beijing") public List<User> select(){ return userService.list(); } @Autowired @DS("shanghai") public List<User> insert(){ userService.save(new User("lihw")); } }
方案三:dynamic-datasource【开源组件实现多数据源】🚩
dynamic-datasource: 基于 SpringBoot 多数据源 动态数据源 主从分离 快速启动器 支持分布式事务 (gitee.com)
1. pom依赖
spring-boot 1.5.x 2.x.x
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
spring-boot 1.5.x 2.x.x
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${version}</version>
</dependency>
2. 配置数据源
- primary: master #设置默认的数据源或者数据源组,默认值即为master
- strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
- slave_1:使用_可以看成slave数组,使用的时候
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
3. 使用 @DS 切换数据源
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}