SpringBoot配置多数据源

Stella981
• 阅读 550

SpringBoot配置多数据源

核心技术点

​ 在Spring 2.x 中引入了AbstractRoutingDataSource, 该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。

​ Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性。而这样的方案就会不同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据。

​ Spring2.x的版本中采用Proxy模式,就是我们在方案中实现一个虚拟的数据源,并且用它来封装数据源选择逻辑,这样就可以有效地将数据源选择逻辑从Client中分离出来。Client提供选择所需的上下文(因为这是Client所知道的),由虚拟的DataSource根据Client提供的上下文来实现数据源的选择。

具体的实现如下

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // TODO
        // 重写 determineCurrentLookupKey 方法
    }
}

原理:

// AbstractRoutingDataSource 类
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;
    }

因此分析到,如果lookupKey 为null则会走默认配置,如果没有所谓的默认配置则会报错,如果指定了数据源,则会加载指定的配置数据源

代码编写

去除默认数据源

/** * 1.配置数据库事务 * 2.去除JDBC 自动配置数据源 */
@EnableTransactionManagement
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class KerwinBootsApplication {

    public static void main(String[] args) {
        SpringApplication.run(KerwinBootsApplication.class, args);
    }
}

多数据源配置

// 多数据源配置

# select 库
spring.datasource.select.jdbc-url=jdbc:mysql://127.0.0.1:3306/test1
spring.datasource.select.driverClassName=com.mysql.jdbc.Driver
spring.datasource.select.username=root
spring.datasource.select.password=

# update 库
spring.datasource.update.jdbc-url=jdbc:mysql://127.0.0.1:3306/test2
spring.datasource.update.driverClassName=com.mysql.jdbc.Driver
spring.datasource.update.username=root
spring.datasource.update.password=

配置数据源Bean

@Configuration
public class DataSourceConfig {

    // application.properteis中对应属性的前缀
    @Bean(name = "selectDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.select")
    public DataSource selectDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "updateDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.update")
    public DataSource updateDataSource() {
        return DataSourceBuilder.create().build();
    }
}

构造线程数据源持有者

final class DataSourceContextHolder {

    /*** * ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的 * 通过get和set方法就可以得到当前线程对应的值 */
    private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    static void setDbType(String dbType) {
        CONTEXT_HOLDER.set(dbType);
    }

    static String getDbType() {
        return CONTEXT_HOLDER.get();
    }

    static void clear() { CONTEXT_HOLDER.remove();}
}

复写路由方法

// 名字(dataSource) Primary Priority
@Component
@Primary // 多个DataSource Bean 因此@Primary 将作为首选者
         // @Priority 优先级
         // 多个按类型的dataSource 为了让它找到bean可以给当前bean修改 名称 -> @Component(value = "dataSource")
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    private static Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class);

    @Autowired
    @Qualifier("selectDataSource")
    private DataSource selectDataSource;

    @Autowired
    @Qualifier("updateDataSource")
    private DataSource updateDataSource;

    @Override
    protected Object determineCurrentLookupKey() {
        logger.info("切换数据源: " + DataSourceContextHolder.getDbType());
        return DataSourceContextHolder.getDbType();
    }

    /** * 重写after配置方法, 配置默认数据源 */
    @Override
    public void afterPropertiesSet() {
        Map<Object,Object> map = new HashMap<>();
        map.put("selectDataSource", selectDataSource);
        map.put("updateDataSource", updateDataSource);
        setTargetDataSources(map);
        setDefaultTargetDataSource(updateDataSource);
        super.afterPropertiesSet();
    }
}

考虑自动切换数据源方案 - AOP (注解或依据方法名)

@Aspect
@Component
@Order(0) // Order设定AOP执行顺序 使之在数据库事务上先执行
public class DynamicDataSourceAspect {

    @Before("execution(* com.boot.service.*.*(..))")
    public void processMethodName (JoinPoint joinPoint) {
        String methodName=joinPoint.getSignature().getName();
        if (methodName.startsWith("get")
                ||methodName.startsWith("count")
                ||methodName.startsWith("find")
                ||methodName.startsWith("list")
                ||methodName.startsWith("select")
                ||methodName.startsWith("check")){
            DataSourceContextHolder.setDbType("selectDataSource");
        }else {
            //切换dataSource
            DataSourceContextHolder.setDbType("updateDataSource");
        }
    }

// @Before("execution(* com.boot.service.*.*(..))")
// public void process(JoinPoint point) {
//
// //获得当前访问的class
// Class<?> className = point.getTarget().getClass();
//
// //获得访问的方法名
// String methodName = point.getSignature().getName();
//
// //得到方法的参数的类型
// Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
//
// try {
// // 得到访问的方法对象
// Method method = className.getMethod(methodName, argClass);
//
// // 判断是否存在@DS注解
// if (method.isAnnotationPresent(DS.class)) {
// DS annotation = method.getAnnotation(DS.class);
//
// // 取出注解中的数据源名
// String dataSource = annotation.value();
//
// // 切换数据源
// DataSourceContextHolder.setDbType(dataSource);
// }
// } catch (Exception e) {
// e.printStackTrace();
// System.out.println("error.");
// }
// }

    @After("execution(* com.boot.service.*.*(..))")
    public void afterswitchDs (JoinPoint point){
        DataSourceContextHolder.clear();
    }
}

遗留技术点

ThreadLocal 的作用,DataSourceContextHolder类的意义何在

作用:建立一个获得和设置上下文环境的类,主要负责改变上下文数据源的名称

原因:ThreadLocal 与 Synchronized 作用不同 -》

Synchronized -> 保证多线程情况下变量一致性(数据共享)

ThreadLocal -> 保证多线程情况下变量私有性(数据隔离)

即每个线程的变量只对自己本线程负责 (不会存在A线程改了影响B的情况,要的就是数据隔离)

官方解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

总结:

总结一下重点:

  • ThreadLocal 提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是当前这个线程私有的,这就是所谓的线程隔离。
  • 如果要使用 ThreadLocal,通常定义为 private static 类型,最好是定义为 private static final 类型。

2.为什么重写了 determineCurrentLookupKey 方法,SpringBoot真正在执行的时候就会调用我们重写的类呢?

// 多数据源方案二代码...核心如下: 此种方案有显示的放入事务数据源中

/** * 配置@Transactional注解 */
@Bean
public PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dynamicDataSource());
}

回顾方案一,跟踪断点发现如下代码:

@Configuration
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcTemplateAutoConfiguration {

    @Configuration
    static class JdbcTemplateConfiguration {

        private final DataSource dataSource;

        private final JdbcProperties properties;

        JdbcTemplateConfiguration(DataSource dataSource, JdbcProperties properties) {
            this.dataSource = dataSource;
            this.properties = properties;
        }

        @Bean
        @Primary
        @ConditionalOnMissingBean(JdbcOperations.class)
        public JdbcTemplate jdbcTemplate() {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource);
            JdbcProperties.Template template = this.properties.getTemplate();
            jdbcTemplate.setFetchSize(template.getFetchSize());
            jdbcTemplate.setMaxRows(template.getMaxRows());
            if (template.getQueryTimeout() != null) {
                jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
            }
            return jdbcTemplate;
        }

    }

    @Configuration
    @Import(JdbcTemplateConfiguration.class)
    static class NamedParameterJdbcTemplateConfiguration {

        @Bean
        @Primary
        @ConditionalOnSingleCandidate(JdbcTemplate.class)
        @ConditionalOnMissingBean(NamedParameterJdbcOperations.class)
        public NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
            return new NamedParameterJdbcTemplate(jdbcTemplate);
        }
    }
}

//*********************************************


@Configuration
@ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class })
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceTransactionManagerAutoConfiguration {

    @Configuration
    @ConditionalOnSingleCandidate(DataSource.class)
    static class DataSourceTransactionManagerConfiguration {

        private final DataSource dataSource;

        private final TransactionManagerCustomizers transactionManagerCustomizers;

        DataSourceTransactionManagerConfiguration(DataSource dataSource,
                ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
            this.dataSource = dataSource;
            this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable();
        }

        @Bean
        @ConditionalOnMissingBean(PlatformTransactionManager.class)
        public DataSourceTransactionManager transactionManager(DataSourceProperties properties) {
            DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource);
            if (this.transactionManagerCustomizers != null) {
                this.transactionManagerCustomizers.customize(transactionManager);
            }
            return transactionManager;
        }
    }
}

我们发现SpringBoot,当注入了唯一DataSource Bean之后,会调用我们创建的指定数据源,将其放入boot核心代码中,之后事务数据源,JDBC数据源都会引用我们注入的Bean,因此我们重写之后,注入完成,SpringBoot真正在执行的时候就会调用我们重写的类

3.为什么要使用@Primary 注解,有没有其他的方案

DataSource Bean 需要被初始化,作为数据库连接所使用,但是在类 DataSourceConfig 中,有两个bean都是DataSource,且 DynamicRoutingDataSource的本质也是一个 DataSource

因此 Spring容器在真正调用DataSource时,会通过类型找到此Bean,但是由于有三个同类型的Bean,因此无法确定,所以又会按名称查找,但是还是找不到,所以如果无法确定到底哪个Bean 被用作数据源连接,则会抛出异常

解决方案有三种

// 多个DataSource Bean 因此@Primary 将作为首选者
// @Priority 优先级
// 多个按类型的dataSource 为了让它找到bean可以给当前bean修改 名称 -> @Component(value = "dataSource")
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
SpringBoot配置多数据源
SpringBoot配置多数据源核心技术点​在Spring2.x中引入了AbstractRoutingDataSource,该类充当了DataSource的路由中介,能有在运行时,根据某种key值来动态切换到真正的DataSource上。​Spring动态配置多数
Stella981 Stella981
2年前
BeetlSQL 3.0.10 发布,多数据源分布式sega事务支持
本次发布主要增加了分布式Sega事务支持,适合多数据源按照社区建议,修改了了springboot的yml配置方式修改了@Jackson和@UpdateTime,本来是用来作为例子,但社区开发者提供了较好的完整实现增加Sega支持<dependency<groupIdcom.ibeetl</gr
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这