当前位置: 首页 > 后端技术 > Java

Spring是如何支持多数据源的

时间:2023-04-01 21:20:32 Java

大家好,我是课代表。欢迎来到我的公众号:Java课代表。上一篇介绍了数据源的基础知识,基于两套DataSource和两套mybatis配置实现了多数据源,从基础知识层面讲解了多数据源的实现思路。不知道的同学请戳→同学,你的多数据源交易无效!文末评论中提到,这种多数据源的方式对代码的侵入性很大,每个组件需要分两套写,不适合大规模的线上实践。对于多数据源的需求,Spring早在2007年就注意到并给出了解决方案。参见原文:dynamic-datasource-routingSpring提供了一个AbstractRoutingDataSource类来实现多DataSources的按需路由。本文介绍的是基于该方法的多数据源实践。1.什么是AbstractRoutingDataSource首先看一下类上的注释:抽象{@linkjavax.sql.DataSource}实现,它根据查找键将{@link#getConnection()}调用路由到各种目标DataSources之一。后者通常(但不一定)通过一些线程绑定的事务上下文来确定。类代表翻译:这是一个抽象类,可以通过查找键将对getConnection()方法的调用路由到目标数据源。后者(指查找键)通常由线程绑定的上下文决定。这段解说可谓字字珠玑,一句废话都没有。下面结合主要代码解释其含义。publicabstractclassAbstractRoutingDataSourceextendsAbstractDataSourceimplementsInitializingBean{//目标DataSourceMap,可以安装很多DataSource@NullableprivateMaptargetDataSources;@NullableprivateMapresolvedDataSources;//Bean初始化时,将targetDataSources遍历并解析后放入resolvedDataSources@OverridepublicvoidafterPropertiesSet(){if(this.targetDataSources==null){thrownewIllegalArgumentException("Property'targetDataSources'isrequired");}this.resolvedDataSources=CollectionUtils.newHashMap(this.targetDataSources.size());this.targetDataSources.forEach((key,value)->{ObjectlookupKey=resolveSpecifiedLookupKey(key);DataSourcedataSource=resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey,dataSource);});如果(this.defaultTargetDataSource!=null){this.resolvedDefaultDataSource=resolveSpecifiedDataSource(this.defaultTargetDataSource);}}@OverridepublicConnectiongetConnection()抛出SQLException{returndetermineTargetDataSource().getConnection();}/***检索当前目标DeminerDataSources*{@link#determineCurrentLookupKey()当前查找键},执行*在{@link#setTargetDataSourcestargetDataSources}映射中查找,*回退到指定的*{@link#setDefaultTargetDataSource默认目标DataSource}如有必要。*@see#determineCurrentLookupKey()*///根据#determineCurrentLookupKey()返回的lookupkey,从解析后的数据源Map中获取对应的数据源。protectedDataSourcedetermineTargetDataSource(){Assert.notNull(this.resolvedDataSources,"数据源路由器未初始化");//当前lookupKey的值由用户实现↓ObjectlookupKey=determineCurrentLookupKey();DataSourcedataSource=this.resolvedDataSources.get(lookupKey);if(dataSource==null&&(this.lenientFallback||lookupKey==null)){dataSource=this.resolvedDefaultDataSource;}if(dataSource==null){thrownewIllegalStateException("CannotdeterminetargetDataSourceforlookupkey["+lookupKey+"]");}返回数据源;}/***确定当前查找键。这通常会*实现以检查线程绑定的事务上下文。*

允许任意键。返回的键需要*匹配存储的查找键类型,由*{@link#resolveSpecifiedLookupKey}方法解析。*///该方法用于确定查找键,通常由线程绑定@NullableprotectedabstractObjectdetermineCurrentLookupKey();//省略其余代码...}首先看类图是一个DataSource,实现了InitializingBean,说明有Bean初始化操作其次,看实例变量privateMaptargetDataSources;和privateMapresolvedDataSources;其实都是一样的东西,后者是解析前者得到的,本质上是用来存储多个DataSource实例的Map。最后看下使用DataSource的核心方法,其实质就是调用它的getConnection()方法获取一个连接,从而进行数据库操作。AbstractRoutingDataSource#getConnection()方法首先调用determineTargetDataSource()来决定使用哪个目标数据源,并使用数据源的getConnection()连接数据库:@OverridepublicConnectiongetConnection()throwsSQLException{returndetermineTargetDataSource().getConnection();}protectedDataSourcedetermineTargetDataSource(){Assert.notNull(this.resolvedDataSources,"DataSource路由器未初始化");//这里使用的lookupKey可以判断返回的数据源是哪个Object。lookupKey=determineCurrentLookupKey();数据源dataSource=this.resolvedDataSources.get(lookupKey);if(dataSource==null&&(this.lenientFallback||lookupKey==null)){dataSource=this.resolvedDefaultDataSource;}if(dataSource==null){thrownewIllegalStateException("无法确定查找键的目标数据源["+lookupKey+"]");}returndataSource;}所以重点在determineCurrentLookupKey()方法上,这是一个由用户实现的抽象方法。通过改变它的返回值,控件返回不同的数据源。形式如下:lookupKeyDataSourcefirstfirstDataSourcesecondsecondDataSource这个方法如何实现?结合Spring在注释中给出的提示:后者(指lookupkey)通常由线程绑定的上下文决定。应该可以想到ThreadLocal!ThreadLocal可以维护一个绑定到当前线程的变量作为这个线程的上下文。2.外部化设计yaml文件,配置多个数据源spring:datasource:first:driver-class-name:org.h2.Driverjdbc-url:jdbc:h2:mem:db1username:sapassword:second:driver-class-name:org.h2.Driverjdbc-url:jdbc:h2:mem:db2username:sapassword:创建lookupKey的contextholdingclass:/***数据源keycontext*通过控制ThreadLocal变量LOOKUP_KEY_HOLDER的值控制数据源切换*@seeRoutingDataSource*@author:Java类代表*/publicclassRoutingDataSourceContext{privatestaticfinalThreadLocalLOOKUP_KEY_HOLDER=newThreadLocal<>();publicstaticvoidsetRoutingKey(StringroutingKey){LOOKUP_KEY_HOLDER.set(routingKey);}publicstaticStringgetRoutingKey(){Stringkey=LOOKUP_KEY_HOLDER.get();//默认返回key为first的数据源returnkey==null?“第一”:钥匙;}publicstaticvoidreset(){LOOKUP_KEY_HOLDER.remove();}}实现AbstractRoutingDataSource:/***支持动态切换的数据源*通过重写determineCurrentLookupKey实现数据源切换*@author:Java类代表*/publicclassRoutingDataSourceextendsAbstractRoutingDataSource{@OverrideprotectedObjectdetermineCurrentLookupKey(){returnRoutingDataSourceContext.getRoutingKey();}}为我们的RoutingDataSource初始化多个数据源:/***数据源配置*将多个数据源组装成一个RoutingDataSource*@author:Javaclassrepresentative*/@ConfigurationpublicclassRoutingDataSourcesConfig{@Bean@ConfigurationProperties(prefix="spring.datasource.first")publicDataSourcefirstDataSource(){returnDataSourceBuilder.create().build();}@Bean@ConfigurationProperties(prefix="spring.datasource.second")publicDataSourcesecondDataSource(){returnDataSourceBuilder.create().build();}@Primary@BeanpublicRoutingDataSourceroutingDataSource(){RoutingDataSourceroutingDataSource=newRoutingDataSource();routingDataSource.setDefaultTargetDataSource(firstDataSource());MapdataSourceMap=newHashMap<>();dataSourceMap.put("第一",firstDataSource());dataSourceMap.put("second",secondDataSource());routingDataSource.setTargetDataSources(dataSourceMap);返回路由数据源;}}演示手动切换的代码:publicvoidinit(){//先手动切换到数据源,初始化表RoutingDataSourceContext.setRoutingKey("first");创建表用户();RoutingDataSourceContext.reset();//手动切换对于数据源second,初始化表RoutingDataSourceContext.setRoutingKey("second");创建表用户();RoutingDataSourceContext.reset();}这样,最基本的多数据源切换不难发现,切换的工作显然可以抽取一段,我们可以优化一下,用注解来表示切入点,需要裁剪的地方。3.引入AOP自定义注解/***@author:Java类代表*/@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceWithDataSource{Stringvalue()default"";}创建切面@Aspect@Component//指定的优先级高于@Transactional默认的优先级//保证先切换数据源再进行事务操作@Order(Ordered.LOWEST_PRECEDENCE-1)publicclassDataSourceAspect{@Around("@annotation(withDataSource)")publicObjectswitchDataSource(ProceedingJoinPointpjp,WithDataSourcewithDataSource)throwsThrowable{//1.获取@WithDataSource注解中指定的数据源StringroutingKey=withDataSource.value();//2.设置数据源上下文RoutingDataSourceContext.setRoutingKey(routingKey);//3.使用设置的数据源处理业务try{returnpjp.proceed();}finally{//4.清除数据源上下文RoutingDataSourceContext.reset();}}}有了注解和切面,使用起来就方便多了://注解表示使用“第二”数据源@WithDataSource("second")publicListgetAllUsersFromSecond(){Listusers=userService.selectAll();returnusers;}aspect有两个细节需要注意:需要指定比声明式事务更高的优先级原因:声明式事务的本质也是AOP,它只对开启时使用的数据源生效,所以切换到指定数据源后必须开启。声明式事务的默认优先级是最低级别。这里,你只需要设置自定义数据源aspect的优先级比它执行完高层业务后必须清除context。原因:假设方法A使用@WithDataSource("second")指定“第二”数据源,后面的方法B没有写注解,期望使用默认的第一个数据源。但是,由于方法A放入context中的lookupKey仍然是“second”,并没有被删除,所以方法B执行的数据源与预期不符。4、回过头来看,基于AbstractRoutingDataSource+AOP的多数据源已经实现。配置DataSourceBean时,使用自定义RoutingDataSource并将其标记为@Primary。这样mybatis-spring-boot-starter就可以使用RoutingDataSource来帮我们自动配置mybatis了,比起两套DataSource+两套Mybatis的配置要简单的多。本文相关代码已上传至班级github代表处。特别说明:为了减少代码层次,展示更直观,事务注解写在controller层。实际开发中不要这样做。controller层的任务就是绑定,校对验证参数,封装返回结果,尽量不要在里面写业务!五、优化对于一般的多数据源使用场景,本文方案覆盖面足够,可以实现灵活切换。但是,仍然存在以下不足:在使用每个应用时,必须添加相关的类,当大量重复修改代码或添加新功能时,必须修改所有相关应用。功能不够强大,也没有高级的功能,比如阅读Loadbalancingofmultipleslavelibrary其实就是把这些代码封装成一个starter,高级的功能可以慢慢扩展。幸运的是,开源世界中已经有了现成的工具。开发mybatis-plus的“baomidou”团队在其生态中开源了一个多数据源框架Dynamic-Datasource。底层原理是AbstractRoutingDataSource,增加了更强大的扩展功能。下篇文章介绍它的使用。