浅析SpringBoot多数据源实现方案

 更新时间:2024年02月20日 15:51:18   作者:半__夏  
现在很多项目的开发过程中,可能涉及到多个数据源,像读写分离的场景,或者因为业务复杂,导致不同的业务部署在不同的数据库上,那么这样的场景,我们应该如何在代码中简洁方便的切换数据源呢,本文介绍SpringBoot多数据源实现方案,感兴趣的朋友跟随小编一起看看吧

SpringBoot多数据源实现方案

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

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

构建多个数据源

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

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

springboot配置文件

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

注册多个数据源

@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的确定逻辑。这个其实是对模板方法模式的典型应用,核心代码如下:

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()方法。

/**
 * 继承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就可以做到。

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();
    }
}
@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了?说干就干

/**
 * 自定义注解用来选择数据源
 *
 * @author HXY
 * @since 1.0.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    DataSourceEnum key() default DataSourceEnum.MASTER;
}
public enum DataSourceEnum {
    MASTER,
    SLAVE,
    ;
}
@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();
        }
    }
}

到此这篇关于SpringBoot多数据源实现方案的文章就介绍到这了,更多相关SpringBoot多数据源内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 深入探究Java多线程并发编程的要点

    深入探究Java多线程并发编程的要点

    这篇文章主要介绍了深入探究Java多线程并发编程的要点,包括关键字synchronized的使用和wait()与notify()获取对象锁的三种方式,需要的朋友可以参考下
    2015-11-11
  • 浅谈将JNI库打包入jar文件

    浅谈将JNI库打包入jar文件

    这篇文章主要介绍了浅谈将JNI库打包入jar文件,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • Java中性能优化的35种方法汇总

    Java中性能优化的35种方法汇总

    很多同学在日常写Java的时候很少去关心性能问题,但是在我们写代码的过程中必须考虑到性能对程序的影响。小到我们使用位运算来实现算术运算,大到我们对 Java 代码的总体架构设计,性能其实离我们很近。本文介绍了Java中性能优化的35种方法,需要的朋友可以参考下。
    2017-01-01
  • MyBatis动态SQL之<choose><when><otherwise>标签的使用

    MyBatis动态SQL之<choose><when><otherwise>标签的使用

    MyBatis中动态语句choose-when-otherwise 类似于Java中的switch-case-default语句,本文就来介绍一下MyBatis动态SQL之<choose><when><otherwise>标签的使用,感兴趣的可以了解一下
    2023-09-09
  • Java抽奖抢购算法

    Java抽奖抢购算法

    这篇文章主要为大家详细介绍了Java抽奖抢购算法,ava实现的抽奖抢购算法,用数据库行锁实现,支持集群,感兴趣的小伙伴们可以参考一下
    2016-08-08
  • java优先队列PriorityQueue中Comparator的用法详解

    java优先队列PriorityQueue中Comparator的用法详解

    这篇文章主要介绍了java优先队列PriorityQueue中Comparator的用法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Java布隆过滤器的应用实例

    Java布隆过滤器的应用实例

    这篇文章主要介绍了Java布隆过滤器的应用实例,在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题,如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题,需要的朋友可以参考下
    2023-11-11
  • Spring Boot整合Kafka教程详解

    Spring Boot整合Kafka教程详解

    这篇文章主要为大家介绍了Spring Boot整合Kafka教程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Java接口回调的本质详解

    Java接口回调的本质详解

    大家好,本篇文章主要讲的是Java接口回调的本质详解,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下
    2022-02-02
  • IDEA使用JDBC安装配置jar包连接MySQL数据库

    IDEA使用JDBC安装配置jar包连接MySQL数据库

    这篇文章介绍了IDEA使用JDBC安装配置jar包连接MySQL数据库的方法,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-01-01

最新评论