浅析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多数据源内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!