基于 spring的动态数据源管理

127 阅读2分钟

业务背景

数据库访问压力大时,常用做法是使用 redis缓存,当数据库写的压力大,需要增加数据库节点,根据压力大小落地不同的方案,例如读写分离,一主多从,多主多从等.

Spring AbstractRoutingDataSource 类介绍

AbstractRoutingDataSource 是一个抽象类,它实现了 Spring Framework 的 DataSource 接口。它可以用来实现动态数据源的功能。 在传统的应用程序中,通常只会配置一个静态的数据源,用于与数据库进行交互。但是在一些特殊的场景下,可能需要根据不同的条件或者业务需求来动态地切换数据源,比如多租户系统、读写分离等。

AbstractRoutingDataSource 的实现思路是,通过一个 ThreadLocal 变量来保存当前线程所使用的数据源标识。当需要获取数据源时,AbstractRoutingDataSource 会根据当前线程的数据源标识选择相应的数据源,并将其返回

AbstractRoutingDataSourced 的 diagram

image.png

实现步骤

  1. 在配置文件中定义各个数据源的 Bean,可以是不同的 DataSource 对象,也可以是不同的 DriverManagerDataSource 对象
package com.github.dynamic.datasource.configuration;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;

public class CustomDynamicDataSource extends AbstractRoutingDataSource {
  private static final ThreadLocal<String> CONTEXT_HOLDER =ThreadLocal.withInitial(String::new);

   public CustomDynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDatasource){

       super.setDefaultTargetDataSource(defaultDataSource);
       super.setTargetDataSources(targetDatasource);
       super.afterPropertiesSet();

   }


    @Override

    protected Object determineCurrentLookupKey() {

        return getDataSource();

    }

    public static void setDataSource(String dataSourceName) {
        CONTEXT_HOLDER.set(dataSourceName);

    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();

    }


    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();

    }

}
  1. 在配置文件中配置 AbstractRoutingDataSource Bean,并将各个数据源的 Bean 注入到 AbstractRoutingDataSource 中
package com.github.dynamic.datasource.configuration;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.github.dynamic.datasource.annotation.DataSourceAspectProcessor;
import com.github.dynamic.datasource.constant.DataSourceNameConstant;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DynamicDataSourceConfigAutoConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder
                .create()
                .build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource(){
        return DruidDataSourceBuilder
                .create()
                .build();
    }

    @Bean
    @Primary
    public CustomDynamicDataSource dynamicRoutingDatasource(DataSource masterDataSource, DataSource slaveDataSource){
        Map<Object, Object> targetDatasource = new HashMap<>(2);
        targetDatasource.put(DataSourceNameConstant.MASTER, masterDataSource);
        targetDatasource.put(DataSourceNameConstant.SLAVE, slaveDataSource);
        return new CustomDynamicDataSource(masterDataSource, targetDatasource);

    }

    @Bean
    public DataSourceAspectProcessor dataSourceAspectProcessor(){
        return new DataSourceAspectProcessor();
    }

}
  1. 基于Aop代理去做数据源动态切换
package com.github.dynamic.datasource.annotation;

import com.github.dynamic.datasource.configuration.CustomDynamicDataSource;

import com.github.dynamic.datasource.constant.DataSourceNameConstant;

import lombok.extern.slf4j.Slf4j;

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.Ordered;

import java.lang.reflect.Method;



@Slf4j
@Aspect
public class DataSourceAspectProcessor implements AutoCloseable, Ordered {

    @Pointcut("@annotation(DSRouter)")

    public void pointcut(){

    }



    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DSRouter router = method.getAnnotation(DSRouter.class);

        if (router == null) {
            CustomDynamicDataSource.setDataSource(DataSourceNameConstant.MASTER);
            log.debug("set datasource is " + DataSourceNameConstant.MASTER);

        } else {
            CustomDynamicDataSource.setDataSource(router.value());
            log.debug("set datasource is " + router.value());
        }

        try {
            return point.proceed();
        } finally {
            close();
            log.debug("clean datasource");

        }
    }

    @Override

    public int getOrder() {
        return 1;

    }


    @Override
    public void close() throws Exception {
        CustomDynamicDataSource.clearDataSource();

    }

}

这是比较简单的做法,也比较实用. 完整代码放在了 gitee上面 gitee.com/zerosj/dyna…