SpringBoot多数据源实现方案

SpringBoot多数据源实现方案

现在很多项目的开发过程中,可能涉及到多个数据源,像读写分离的场景,或者因为业务复杂,导致不同的业务部署在不同的数据库上,那么这样的场景,我们应该如何在代码中简洁方便的切换数据源呢?分析这个需求,我们发现要做的事情无非两件

  1. 构建多个数据源
  2. 封装一个模块能实现动态的切换数据源,且数据源的切换代码应该尽量和业务进行解耦

构建多个数据源

构建多个数据源其实比较简单,和构建一个数据源是类似的。在SpringBoot中,只需要做三件事

  1. 将数据库的配置注册到配置文件中
  2. 选择一个数据库连接池来构建数据源,我们这里用阿里出品的Druid
  3. 选择一个orm框架来实现基本的sql,我们这里选用Mybatis

springboot配置文件

yaml 复制代码
spring:
  datasource:
    master:
      url: jdbc:mysql://localhost:3306/db_master
      username: root
      password: ******
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
    slave:
      url: jdbc:mysql://localhost:3306/db_slave
      username: root
      password: Hxy@950504
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/**/*.xml

注册多个数据源

java 复制代码
@Configuration
public class DataSourceConfig {

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

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

动态切换数据源

spring提供的方案

关于动态切换数据源,spring给我们提供了一套解决方案,主要通过AbstractRoutingDataSource类实现,这个类是一个抽象类,每次和数据库的交互都会调用该类的getConnection() 方法获取数据库连接,而getConnection() 方法会调用determineCurrentLookupKey先选择一个正确的数据源,数据源如何选择呢?他的具体实现是,由我们开发人员提前将所有的数据源通过K-V的格式放到一个map中,V是具体的数据源,K是数据源的唯一标识。然后将这个map交给AbstractRoutingDataSource去管理,在需要路由的时候他会根据给定的K从map中匹配对应的数据源。那么K又怎么来呢?哪个接口应该用哪个key呢?AbstractRoutingDataSource给我们提供了一个抽象方法determineTargetDataSource(),供我们自定义实现key的确定逻辑。这个其实是对模板方法模式的典型应用,核心代码如下:

java 复制代码
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    // map结构,用来保存所有的数据源
	@Nullable
	private Map<Object, Object> targetDataSources;
    
    // 默认的数据源
	@Nullable
	private Object defaultTargetDataSource;

	@Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}


	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	/**
	 * getConnection()方法和determineTargetDataSource()方法定义了获取数据库连接,选择数据源的核心逻辑
	 */
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		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 + "]");
		}
		return dataSource;
	}

	/**
	 * 根据key选择数据源,但是哪个接口用那个key,由用户自己决定,这就是模板方法模式
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();

}

构建动态数据源

在了解了上述的基本原理后,我们就可以着手构建我们的动态数据源啦,首先自定义一个类继承AbstractRoutingDataSource,实现determineCurrentLookupKey()方法。

java 复制代码
/**
 * 继承spring提供的多数据源路由类,初始化默认数据源和实现选择数据源的方法
 *
 * @author HXY
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    // 有参构造器,初始化所有的数据源和默认数据源
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> allDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(allDataSource);
        super.afterPropertiesSet();
    }

    // 实现抽象方法,定义我们获取K的逻辑
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

DataSourceContextHolder类使用ThreadLocal来存储当前线程使用的数据源名称。通过setDataSourceKey()方法设置数据源名称,通过getDataSourceKey()方法获取数据源名称,通过clearDataSourceKey()方法清除数据源名称。

这里用ThreadLocal的主要原因是为了做多并发线程隔离 ,比如同一时间可能会有很多请求并发进来,假设有10个请求,然后系统分配线程1处理请求1,请求1需要用mster数据源,线程2处理请求2,请求2需要用slave数据源。他们可能同时在进行,那么我们如何将这些请求需要的key做线程隔离呢,使之不互相影响呢?ThreadLocal就可以做到。

java 复制代码
public class DataSourceContextHolder {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

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

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

    public static void release() {
        CONTEXT_HOLDER.remove();
    }
}
java 复制代码
@Configuration
public class DataSourceConfig {

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

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

    // DynamicDataSource要交给spring管理
    @Primary  // 一定要写,让DynamicDataSource被容器优先选择
    @Bean
    public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        // 所有数据源放到一个map中,交给动态数据源管理
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DataSourceEnum.MASTER.name(), masterDataSource);
        targetDataSources.put(DataSourceEnum.SLAVE.name(), slaveDataSource);
		
        // 默认数据源、所有数据源
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
}

通过切面将业务和数据源切换模块解耦

现在动态数据源切换的方案有了,那么如何能将每一个请求路由的到正确的数据源,而且将这些和业务无关的代码和业务进行解耦呢。是的,我们可以用aop,构建一个切面,在实现一个自定义注解,将注解标记在需要切换数据源的接口上,让每一个请求处理之前先去选择数据源,在处理业务逻辑,最后返回结果是不是就OK了?说干就干

java 复制代码
/**
 * 自定义注解用来选择数据源
 *
 * @author HXY
 * @since 1.0.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DataSourceEnum key() default DataSourceEnum.MASTER;
}
java 复制代码
public enum DataSourceEnum {

    MASTER,

    SLAVE,
    ;
}
java 复制代码
@Aspect
@Component
public class DynamicDataSourceAspect {

    // 用环绕通知拦截标记了DataSource注解的方法,方法执行前选择数据源,然后执行原来的方法,最后返回结果
    @Around("@annotation(dataSource)")
    public Object selectDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable {
        try {
            String selectKey = dataSource.key().name();
            DataSourceContextHolder.setDataSource(selectKey);
            return joinPoint.proceed();
        } finally {
            // 请求处理完成后一定要及时释放ThreadLocal数据,否则会引起内存泄漏
            DataSourceContextHolder.release();
        }
    }
}
相关推荐
一只叫煤球的猫1 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9652 小时前
tcp/ip 中的多路复用
后端
bobz9652 小时前
tls ingress 简单记录
后端
你的人类朋友3 小时前
什么是OpenSSL
后端·安全·程序员
bobz9653 小时前
mcp 直接操作浏览器
后端
程序新视界4 小时前
MySQL中什么是回表查询,如何避免和优化?
mysql
前端小张同学6 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook6 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康7 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在7 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net