一、了解AbstractRoutingDataSource
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 源码的介绍:
大概意思是:
AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。
AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。
实现逻辑:
1.定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
2.把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
3.调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。
二:具体实现
1.配置依赖
spring常规依赖
<properties>
<java.version>1.8</java.version>
<pagehelper.boot.version>1.2.5</pagehelper.boot.version>
<druid.version>1.1.14</druid.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- SpringBoot 拦截器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- pagehelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
SpringBootApplication.java
取消自动数据源配置
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
或者加上
@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class SpringBootApplication{
public static void main(String[] args) {
SpringApplication.run(SpringBootApplication.class, args);
}
}
配置文件参数
application.yml(并引入相关数据源的yml文件)
server:
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
uri-encoding: UTF-8
threads:
max: 400
min-spare: 20
spring:
profiles:
# 额外读取datasource配置
active: datasource
mybatis:
# 搜索指定包别名
type-aliases-package: com.lg.**.dto
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapper-locations: classpath:/mybatis/**/*.xml
# 加载全局的配置文件
config-location: classpath:mybatis-config.xml
application-datasource.yml--数据源相关配置
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# 主数据库
master:
jdbc-url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
# 其他数据库(从数据库)
slave:
enabled: true
jdbc-url: jdbc:mysql://localhost:3306/slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
# 初始连接数
initial-size: 5
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
#max-evictable-idle-time-millis: 900000
# 配置检测连接是否有效
validation-query: select 1 from dual
test-while-idle: true
test-on-borrow: false
test-on-return: false
通过AbstractRoutingDataSource实现数据源动态切换
Springboot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,切换到需要的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
/**
*
* 数据源枚举类
*
*/
public enum DataSourceType {
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}
配置数据源切换注解,在具体类或方法上添加注解,标明要使用的数据库
/**
*
* 数据源切换注解
* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
*
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/**
* 切换数据源名称
*/
DataSourceType value() default DataSourceType.SLAVE;
}
配置缓存数据源
package com.db.system.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.db.system.utils.DataSourceType;
/**
* 数据源切换处理
* @Package: com.db.system.config
* @ClassName: DynamicDataSourceContextHolder.java
* @Description:数据源切换处理
*
* @author kaifa008
* @date 2022年1月
*/
public class DynamicDataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(DataSourceType dataSourceType) {
logger.info("切换到{{}}数据源", dataSourceType);
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获得数据源的变量
*/
public static DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
继承AbstractRoutingDataSource
package com.db.system.config;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态切换数据源
* @Package: com.db.system.config
* @ClassName: DynamicDataSource.java
* @Description:动态切换数据源
*
* @author kaifa008
* @date 2022年1月
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
super.afterPropertiesSet();
}
/**
* 获取数据源的信息
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
determineCurrentLookupKey在每次取数据源时,决定当前取哪个数据源。
该方法返回需要使用的DataSource的key值,然后根据这个key从resolvedDataSources这个map里取出对应的DataSource,如果找不到,则用默认的resolvedDefaultDataSource
配置多数据源config
package com.db.system.config;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
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 com.db.system.utils.DataSourceType;
/**
* 配置多数据源(Druid)
* @Package: com.db.system.config
* @ClassName: DataSourceConfig.java
* @Description:配置多数据源(Druid)
*
* @author kaifa008
* @date 2022年1月
*/
@Configuration
public class DataSourceConfig {
/**
* 查看配置文件中数据库连接池主从
*/
@Value("${dataSource.dbType}")
private String dbType;
@Bean(name="MASTER")
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name="SLAVE")
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource() {
if (StringUtils.isEmpty(dbType)) {
dbType = "SLAVE";
}
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource());
targetDataSources.put(DataSourceType.MASTER, masterDataSource());
DataSource defaultTargetDataSource = null;
switch (dbType) {
case "SLAVE":
defaultTargetDataSource = (DataSource) targetDataSources.get(DataSourceType.SLAVE);
break;
case "MASTER":
defaultTargetDataSource = (DataSource) targetDataSources.get(DataSourceType.MASTER);
break;
default:
defaultTargetDataSource = (DataSource) targetDataSources.get(DataSourceType.SLAVE);
break;
}
// DynamicDataSource dynamicDataSource = new DynamicDataSource();
// dynamicDataSource.setDefaultTargetDataSource(defaultTargetDataSource);
// dynamicDataSource.setTargetDataSources(targetDataSources);
// return dynamicDataSource;
return new DynamicDataSource(defaultTargetDataSource, targetDataSources);
}
}
配置了多个数据源,并通过读取yml参数配置初始化,可以通过yml优先使用哪个数据源。
DynamicDataSource就是上面覆写AbstractRoutingDataSource的类。
配置aop
package com.db.system.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.db.system.config.DynamicDataSourceContextHolder;
import com.db.system.interfaces.DataSource;
/**
* 数据源注解AOP拦截并切换
* @Package: com.db.system.aop
* @ClassName: DataSourceAspect.java
* @Description:数据源注解AOP拦截并切换
*
* @author kaifa008
* @date 2022年1月
*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
// @Pointcut("@annotation(dataSource) || @within(dataSource)")
@Pointcut("@annotation(com.db.system.interfaces.DataSource)")
public void dataSourcePointCut() {
}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (dataSource != null) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value());
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
/**
* 获取需要切换的数据源
* @param point
* @return
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (dataSource != null) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
通过注解配置的切入点,进行注解读取,然后选择对应的数据源。
@Around("dataSourcePointCut()")
或者使用@Before("dataSourcePointCut())")和@After("dataSourcePointCut()")
执行方法前后设置数据源
应用层配置
通过上述配置,多数据源配置完成了。就可以在controller里使用了。
/**
* 应用层实现
*/
@RestController
@RequestMapping(value = "/")
public class UserController {
@Autowired
private UserService userService;
/**
* 测试配置文件指定
*/
@RequestMapping(value = "listAllUser")
public List<User> listAllUser() {
return userService.listAllUser();
}
/**
* 测试默认数据源
*/
@DataSource
@RequestMapping(value = "listAllUserSlave")
public List<User> listAllUserSlave() {
return userService.listAllUser();
}
/**
* 测试注解指定数据源
*/
@DataSource(value = DataSourceType.MASTER)
@RequestMapping(value = "listAllUserMaster")
public List<User> listAllUserMaster() {
return userService.listAllUser();
}
/**
* 测试方法内指定数据源
*/
@RequestMapping(value = "listAllUserMaster")
public List<User> listAllUserMaster() {
DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
List<User> userList = userService.listAllUser();
DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
return userService.listAllUser();
}
}
三:关于事务
AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。 spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。
一般Spring管理事务是放在Service业务层操作的,所以更换数据源的操作要放在这个操作之前进行。也就是切换数据源操作放在Controller层。