随着业务的不断深入,我们会碰见很多关于数据源切换的业务场景,数据源切换也是当前最常用的分库后的分流策略方式之一,对于读写职责分离的数据库集群而言,我们在服务层面制定相应的接口与数据库交互的定制化开发,也就是我们可以自己通过程序控制当前的接口访问的数据库的地址与数据源的实例对象。对于同一个业务系统使用到不同服务器的数据库而言,我们也可以动态的切换数据源,从而做到预先准备,用时切换的功能。当然数据源切换也只是一种解决方案,并不是最合适的实践思路,本文旨在与大家分享常用的数据源切换的实践思路。
AbstractRoutingDataSource核心抽象类
字面意思上,抽象路由数据源,其中的源码如下:
核心属性与实现的核心拓展接口InitializingBean(属性填充之前实例化之前)
核心的某些设置目标数据源方法(透传给容器,或感知到容器的某些特性)
下面就是重写Initializing接口的实现方法afterProptriesSet()属性填充之后的bean的自定义逻辑
核心的切换数据源的方法,以及可以从中看出如何获取数据源的方法
那么我们看完源码大概能了解到determineCurrentLookupKey方法就是我们核心的修改数据源的方法,只要我们在这个方法的实现里稍微修改一下KEY的返回逻辑,是不是就能够起到切换数据源的逻辑?
由此我们设计一个自定义的动态数据源类:
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}
}
这里的DynamicDataSourceHolder类主要的作用是利用ThreadLocal
@Slf4j
public class DynamicDataSourceHolder {
private static final ThreadLocal DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
public static void setDynamicDataSourceKey(String key){
log.info("数据源切换为:{}",key);
DYNAMIC_DATASOURCE_KEY.set(key);
}
public static String getDynamicDataSourceKey(){
String key = DYNAMIC_DATASOURCE_KEY.get();
return key == null ? DataSourceType.WRITE: key;
}
public static void removeDynamicDataSourceKey(){
log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());
DYNAMIC_DATASOURCE_KEY.remove();
}
}
那么切换的逻辑已经基本都交给容器去处理了,我们接下来要实现读取配置文件中的多数据源设置参数,并将这些数据源实例化后交给容器管理。
由此我们设计一个DruidDataSourceConfiguration:
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@ConditionalOnProperty(
name = "spring.datasource.druid.write",
matchIfMissing = true
)
public class DataSourceConfiguration {
@Bean(name= DataSourceType.WRITE)
@Qualifier(DataSourceType.WRITE)
@ConfigurationProperties("spring.datasource.druid.write")
public DruidDataSource wirteDruidDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name= DataSourceType.READ)
@Qualifier(DataSourceType.READ)
@ConfigurationProperties("spring.datasource.druid.read")
public DruidDataSource readDruidDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource()
{
Map
这里主要的调用也是在之前的源码里有提到,我们还定义了一个所谓的接口:
public interface DataSourceType{
String WRITE = "WRITE";
String READ = "READ";
}
接下来我们还要利用注解规定某一个接口是否需要切换数据源:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
/*数据源类型*/
String value() default DataSourceType.READ;
}
通常最常用的方式就是aop去代理接口所属的对象,拿到代理的数据信息进行处理,这里我们是不是可以切换当前数据源到我们注解上过规定的数据源呢?
@Aspect
@Slf4j
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.runjing.learn_runjing.dataSource.DataSource)")
public void point(){}
@Around("point()")
public void setDataSource(ProceedingJoinPoint pointcut) throws Throwable{
String value = getDefineAnnotation(pointcut).value();
DynamicDataSourceHolder.setDynamicDataSourceKey(value);
try{
pointcut.proceed();
}finally {
DynamicDataSourceHolder.removeDynamicDataSourceKey();
}
}
private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);
if (Objects.nonNull(methodSignature)) {
return dataSourceAnnotation;
} else {
Class> dsClass = joinPoint.getTarget().getClass();
return dsClass.getAnnotation(DataSource.class);
}
}
}
以上,一种动态数据源的切换实践方案分享结束。