SpringBoot配置多数据源

SpringBoot配置多数据源

核心技术点

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

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

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

具体的实现如下

1public class DynamicRoutingDataSource extends AbstractRoutingDataSource { 2 @Override 3 protected Object determineCurrentLookupKey() { 4 // TODO 5 // 重写 determineCurrentLookupKey 方法 6 } 7}

原理:

1// AbstractRoutingDataSource 类 2protected DataSource determineTargetDataSource() { 3 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); 4 Object lookupKey = determineCurrentLookupKey(); 5 DataSource dataSource = this.resolvedDataSources.get(lookupKey); 6 if (dataSource == null && (this.lenientFallback || lookupKey == null)) { 7 dataSource = this.resolvedDefaultDataSource; 8 } 9 if (dataSource == null) { 10 throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); 11 } 12 return dataSource; 13 }

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

代码编写

去除默认数据源

1/** * 1.配置数据库事务 * 2.去除JDBC 自动配置数据源 */ 2@EnableTransactionManagement 3@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 4public class KerwinBootsApplication { 5 6 public static void main(String[] args) { 7 SpringApplication.run(KerwinBootsApplication.class, args); 8 } 9}

多数据源配置

1// 多数据源配置 2 3# select 库 4spring.datasource.select.jdbc-url=jdbc:mysql://127.0.0.1:3306/test1 5spring.datasource.select.driverClassName=com.mysql.jdbc.Driver 6spring.datasource.select.username=root 7spring.datasource.select.password= 8 9# update 库 10spring.datasource.update.jdbc-url=jdbc:mysql://127.0.0.1:3306/test2 11spring.datasource.update.driverClassName=com.mysql.jdbc.Driver 12spring.datasource.update.username=root 13spring.datasource.update.password=

配置数据源Bean

1@Configuration 2public class DataSourceConfig { 3 4 // application.properteis中对应属性的前缀 5 @Bean(name = "selectDataSource") 6 @ConfigurationProperties(prefix = "spring.datasource.select") 7 public DataSource selectDataSource() { 8 return DataSourceBuilder.create().build(); 9 } 10 11 @Bean(name = "updateDataSource") 12 @ConfigurationProperties(prefix = "spring.datasource.update") 13 public DataSource updateDataSource() { 14 return DataSourceBuilder.create().build(); 15 } 16}

构造线程数据源持有者

1final class DataSourceContextHolder { 2 3 /*** * ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的 * 通过get和set方法就可以得到当前线程对应的值 */ 4 private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); 5 6 static void setDbType(String dbType) { 7 CONTEXT_HOLDER.set(dbType); 8 } 9 10 static String getDbType() { 11 return CONTEXT_HOLDER.get(); 12 } 13 14 static void clear() { CONTEXT_HOLDER.remove();} 15}

复写路由方法

1// 名字(dataSource) Primary Priority 2@Component 3@Primary // 多个DataSource Bean 因此@Primary 将作为首选者 4 // @Priority 优先级 5 // 多个按类型的dataSource 为了让它找到bean可以给当前bean修改 名称 -> @Component(value = "dataSource") 6public class DynamicRoutingDataSource extends AbstractRoutingDataSource { 7 8 private static Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class); 9 10 @Autowired 11 @Qualifier("selectDataSource") 12 private DataSource selectDataSource; 13 14 @Autowired 15 @Qualifier("updateDataSource") 16 private DataSource updateDataSource; 17 18 @Override 19 protected Object determineCurrentLookupKey() { 20 logger.info("切换数据源: " + DataSourceContextHolder.getDbType()); 21 return DataSourceContextHolder.getDbType(); 22 } 23 24 /** * 重写after配置方法, 配置默认数据源 */ 25 @Override 26 public void afterPropertiesSet() { 27 Map<Object,Object> map = new HashMap<>(); 28 map.put("selectDataSource", selectDataSource); 29 map.put("updateDataSource", updateDataSource); 30 setTargetDataSources(map); 31 setDefaultTargetDataSource(updateDataSource); 32 super.afterPropertiesSet(); 33 } 34}

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

1@Aspect 2@Component 3@Order(0) // Order设定AOP执行顺序 使之在数据库事务上先执行 4public class DynamicDataSourceAspect { 5 6 @Before("execution(* com.boot.service.*.*(..))") 7 public void processMethodName (JoinPoint joinPoint) { 8 String methodName=joinPoint.getSignature().getName(); 9 if (methodName.startsWith("get") 10 ||methodName.startsWith("count") 11 ||methodName.startsWith("find") 12 ||methodName.startsWith("list") 13 ||methodName.startsWith("select") 14 ||methodName.startsWith("check")){ 15 DataSourceContextHolder.setDbType("selectDataSource"); 16 }else { 17 //切换dataSource 18 DataSourceContextHolder.setDbType("updateDataSource"); 19 } 20 } 21 22// @Before("execution(* com.boot.service.*.*(..))") 23// public void process(JoinPoint point) { 24// 25// //获得当前访问的class 26// Class<?> className = point.getTarget().getClass(); 27// 28// //获得访问的方法名 29// String methodName = point.getSignature().getName(); 30// 31// //得到方法的参数的类型 32// Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes(); 33// 34// try { 35// // 得到访问的方法对象 36// Method method = className.getMethod(methodName, argClass); 37// 38// // 判断是否存在@DS注解 39// if (method.isAnnotationPresent(DS.class)) { 40// DS annotation = method.getAnnotation(DS.class); 41// 42// // 取出注解中的数据源名 43// String dataSource = annotation.value(); 44// 45// // 切换数据源 46// DataSourceContextHolder.setDbType(dataSource); 47// } 48// } catch (Exception e) { 49// e.printStackTrace(); 50// System.out.println("error."); 51// } 52// } 53 54 @After("execution(* com.boot.service.*.*(..))") 55 public void afterswitchDs (JoinPoint point){ 56 DataSourceContextHolder.clear(); 57 } 58}

遗留技术点

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真正在执行的时候就会调用我们重写的类呢?

1// 多数据源方案二代码...核心如下: 此种方案有显示的放入事务数据源中 2 3/** * 配置@Transactional注解 */ 4@Bean 5public PlatformTransactionManager transactionManager() { 6 return new DataSourceTransactionManager(dynamicDataSource()); 7}

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

1@Configuration 2@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) 3@ConditionalOnSingleCandidate(DataSource.class) 4@AutoConfigureAfter(DataSourceAutoConfiguration.class) 5@EnableConfigurationProperties(JdbcProperties.class) 6public class JdbcTemplateAutoConfiguration { 7 8 @Configuration 9 static class JdbcTemplateConfiguration { 10 11 private final DataSource dataSource; 12 13 private final JdbcProperties properties; 14 15 JdbcTemplateConfiguration(DataSource dataSource, JdbcProperties properties) { 16 this.dataSource = dataSource; 17 this.properties = properties; 18 } 19 20 @Bean 21 @Primary 22 @ConditionalOnMissingBean(JdbcOperations.class) 23 public JdbcTemplate jdbcTemplate() { 24 JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource); 25 JdbcProperties.Template template = this.properties.getTemplate(); 26 jdbcTemplate.setFetchSize(template.getFetchSize()); 27 jdbcTemplate.setMaxRows(template.getMaxRows()); 28 if (template.getQueryTimeout() != null) { 29 jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); 30 } 31 return jdbcTemplate; 32 } 33 34 } 35 36 @Configuration 37 @Import(JdbcTemplateConfiguration.class) 38 static class NamedParameterJdbcTemplateConfiguration { 39 40 @Bean 41 @Primary 42 @ConditionalOnSingleCandidate(JdbcTemplate.class) 43 @ConditionalOnMissingBean(NamedParameterJdbcOperations.class) 44 public NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) { 45 return new NamedParameterJdbcTemplate(jdbcTemplate); 46 } 47 } 48} 49 50//********************************************* 51 52 53@Configuration 54@ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class }) 55@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) 56@EnableConfigurationProperties(DataSourceProperties.class) 57public class DataSourceTransactionManagerAutoConfiguration { 58 59 @Configuration 60 @ConditionalOnSingleCandidate(DataSource.class) 61 static class DataSourceTransactionManagerConfiguration { 62 63 private final DataSource dataSource; 64 65 private final TransactionManagerCustomizers transactionManagerCustomizers; 66 67 DataSourceTransactionManagerConfiguration(DataSource dataSource, 68 ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) { 69 this.dataSource = dataSource; 70 this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable(); 71 } 72 73 @Bean 74 @ConditionalOnMissingBean(PlatformTransactionManager.class) 75 public DataSourceTransactionManager transactionManager(DataSourceProperties properties) { 76 DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource); 77 if (this.transactionManagerCustomizers != null) { 78 this.transactionManagerCustomizers.customize(transactionManager); 79 } 80 return transactionManager; 81 } 82 } 83}

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

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

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

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

解决方案有三种

1// 多个DataSource Bean 因此@Primary 将作为首选者 2// @Priority 优先级 3// 多个按类型的dataSource 为了让它找到bean可以给当前bean修改 名称 -> @Component(value = "dataSource")
点赞
收藏

评论区

加载中...

相关推荐

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

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )