springboot 实现主从数据库动态切换,可实现读写分离
使用 AbstractRoutingDataSource 实现功能,代码完整贴出,直接放心食用。从AbstractRoutingDataSource源码角度简单分析为什么可以实现数据库动态切换。前言主从数据库的配置,实现数据同步,配置可参考:windows配置mysql8.0主从数据库_追寻光的方向的博客-CSDN博客一、AbstractRoutingDataSourceSpring boot提供
使用 AbstractRoutingDataSource 实现功能,代码完整贴出,直接放心食用。
从AbstractRoutingDataSource源码角度简单分析为什么可以实现数据库动态切换。
前言
主从数据库的配置,实现数据同步,配置可参考:
windows配置mysql8.0主从数据库_追寻光的方向的博客-CSDN博客
一、AbstractRoutingDataSource
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们每次访问数据库之前,设置使用的数据源。从而实现动态切换数据源。
1.源码简单分析
AbstractRoutingDataSource 继承了 AbstractDataSource 类 从而实现了DataSource接口的getConnection()方法。
在AbstractRoutingDataSource 实现 getConnection 的具体方法源码可以看到 ,最终获取的数据源是DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); 这一行。
resolvedDataSources是一个Map对象,就是说动态的数据源是放在这个Map对象的。
Object lookupKey = determineCurrentLookupKey(); 提供一个Map的key来获取
那往下看,我们看下这个 resolvedDataSources 是怎么初始化的,
afterPropertiesSet 方法是 InitializingBean 接口的实现,InitializingBean接口为bean提供了初始化方法的方式,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
但是源码中 resolvedDataSources 的来源是 targetDataSources
那再来看看targetDataSources 是怎么初始化的。
2.源码分析结论
到此,我们思路就有了
1.在每次数据库访问的时候能控制读取存放数据源resolvedDataSources的key值。
2.调用setTargetDataSources方法,来初始化targetDataSources对象。
二、实现步骤
1.创建一个类继承AbstractRoutingDataSource
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
private Logger logger = LogManager.getLogger();
@Override
protected Object determineCurrentLookupKey() {
String dataSource = RoutingDataSourceHolder.getDataSource();
logger.info("使用数据源:{}", dataSource);
return dataSource;
}
}
重写 determineCurrentLookupKey方法,返回要使用的数据源key值。
2.创建一个管理数据源key值得类RoutingDataSourceHolder
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class RoutingDataSourceHolder {
private static Logger logger = LogManager.getLogger();
private static final ThreadLocal<String> dataSources = new ThreadLocal<>();
//一个事务内用同一个数据源
public static void setDataSource(String dataSourceName) {
if (dataSources.get() == null) {
dataSources.set(dataSourceName);
logger.info("设置数据源:{}", dataSourceName);
}
}
public static String getDataSource() {
return dataSources.get();
}
public static void clearDataSource() {
dataSources.remove();
}
}
代码设置了一个事务内使用同一个数据源。
两个类解决了动态数据源key值的问题,下面处理初始化targetDataSources对象。
3.配置主从数据库类DataSourceConfigurer
1.DataSourceConfigurer
import com.alibaba.druid.pool.DruidDataSource;
import com.custom.common.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 配置主从数据库
*/
@Configuration
public class DataSourceConfigurer {
private Logger logger = LogManager.getLogger();
public final static String MASTER_DATASOURCE = "masterDataSource";
public final static String SLAVE_DATASOURCE = "slaveDataSource";
@Bean(MASTER_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource masterDataSource(DataSourceProperties properties) {
DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
logger.info("配置主数据库:{}", build);
return build;
}
@Bean(SLAVE_DATASOURCE)
@ConfigurationProperties(prefix = "spring.slave-datasource")
public DruidDataSource slaveDataSource(DataSourceProperties properties) {
DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
logger.info("配置从数据库:{}", build);
return build;
}
/**
* Primary 优先使用该Bean
* DependsOn 先执行主从数据库的配置
* Qualifier 指定使用哪个Bean
*
* @param masterDataSource
* @param slaveDataSource
* @return
*/
@Bean
@Primary
@DependsOn(value = {MASTER_DATASOURCE, SLAVE_DATASOURCE})
public DataSource routingDataSource(@Qualifier(MASTER_DATASOURCE) DruidDataSource masterDataSource,
@Qualifier(SLAVE_DATASOURCE) DruidDataSource slaveDataSource) {
if (StringUtils.isBlank(slaveDataSource.getUrl())) {
logger.info("没有配置从数据库,默认使用主数据库");
return masterDataSource;
}
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource);
map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
//设置动态数据源
routing.setTargetDataSources(map);
//设置默认数据源
routing.setDefaultTargetDataSource(masterDataSource);
logger.info("主从数据库配置完成");
return routing;
}
}
设置初始化targetDataSources对象关键代码
Map<Object, Object> map = new HashMap<>(); map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource); map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource); RoutingDataSource routing = new RoutingDataSource(); //设置动态数据源 routing.setTargetDataSources(map); //设置默认数据源 routing.setDefaultTargetDataSource(masterDataSource);
2.application.properties
# ----------------------------------------
# 主数据库
# ----------------------------------------
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/custom?useunicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ----------------------------------------
# 从数据库
# ----------------------------------------
spring.slave-datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.slave-datasource.url=jdbc:mysql://127.0.0.1:3309/custom?useunicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
spring.slave-datasource.username=root
spring.slave-datasource.password=root
spring.slave-datasource.driver-class-name=com.mysql.cj.jdbc.Driver
一个配置类处理了targetDataSources对象的初始化.
那问题都处理了,那具体要怎么使用呢,关键就是在事务之前调用RoutingDataSourceHolder.setDataSource()方法就可以了。我们写一个aop实现吧。
4.创建aop注解和类
1.DataSourceWith
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface DataSourceWith {
String key() default "";
}
2.DataSourceWithAspect
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前运行
@Component
public class DataSourceWithAspect {
/**
* 使用DataSourceWith注解就拦截
*/
@Pointcut("@annotation(com.custom.configure.datasource.DataSourceWith)||@within(com.custom.configure.datasource.DataSourceWith)")
public void doPointcut() {
}
/**
* 方法前,为了在事务前设置
*/
@Before("doPointcut()")
public void doBefore(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 获取注解对象
DataSourceWith dataSource = method.getAnnotation(DataSourceWith.class);
if (dataSource == null) {
//方法没有就获取类上的
dataSource = method.getDeclaringClass().getAnnotation(DataSourceWith.class);
}
String key = dataSource.key();
RoutingDataSourceHolder.setDataSource(key);
}
@After("doPointcut()")
public void doAfter(JoinPoint joinPoint) {
RoutingDataSourceHolder.clearDataSource();
}
}
3.使用和效果
@DataSourceWith在方法上或者类上都可以。
/**
* 获取部门列表
**/
@DataSourceWith(key = DataSourceConfigurer.SLAVE_DATASOURCE)
public List<Dept> findDeptTree() {
logger.debug("获取部门树数据");
List<Dept> deptList = new ArrayList<>();
return deptList;
}
结果:动态切换成功
更多推荐
所有评论(0)