其实在系统设计时,应当尽量避免一个项目接入多个数据源。我们应该尽量收敛一个数据库的使用者,这样在后续进行一些数据迁移、数据库重构等工作时能够降低风险和难度。 当然,这并不是绝对的情况,所谓“存在即是合理”。多个数据源的使用从另一方面来说能够大大的降低编码便捷性。我们不再需要通过Dubbo、SpringCloud等方式去通过其他系统中获取相关的数据。正好最近工作中遇到了相应的使用场景,下面来分享下我所考量的两种解决方案:

多数据源方案

其实这种方案是第一时间都能够想到的方案,我们直接在项目中注入多个SqlSessionFactory(如果你使用的是Mybatis的话)并可以将多个数据源的domain、dao按包进行存放。这样通过配置不同的SqlSessionFactory的@MapperScan和setTypeAliasesPackage就可以访问不同数据库了。如下代码片段配置了其中一个数据源的参数:

        /*** db_b2b数据源* @return b2b库数据源*/@Bean(name = "b2b")public DataSource b2b () throws SQLException {MysqlXADataSource mysqlXADataSource=new MysqlXADataSource();mysqlXADataSource.setUrl((dbB2BProperties.getUrl()));mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);mysqlXADataSource.setPassword((dbB2BProperties.getPassword()));mysqlXADataSource.setUser((dbB2BProperties.getUserName()));mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);AtomikosDataSourceBean xaDataSource=new AtomikosDataSourceBean();xaDataSource.setXaDataSource(mysqlXADataSource);xaDataSource.setUniqueResourceName("b2b");return xaDataSource;}@Bean(name = "sqlSessionFactoryB2B")public SqlSessionFactory sqlSessionFactoryB2B(@Qualifier("b2b")DataSource dataSource) throws Exception {MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setTypeAliasesPackage("com.mhc.lite.dal.domain.b2b");MybatisConfiguration configuration = new MybatisConfiguration();configuration.setJdbcTypeForNull(JdbcType.NULL);configuration.setMapUnderscoreToCamelCase(true);configuration.setCacheEnabled(false);bean.setConfiguration(configuration);bean.setPlugins(new Interceptor[]{paginationInterceptor //添加分页功能});return bean.getObject();}@Bean(name = "sqlSessionTemplateB2B")public SqlSessionTemplate sqlSessionTemplateB2B(@Qualifier("sqlSessionFactoryB2B") SqlSessionFactory sqlSessionFactory) throws Exception {return new SqlSessionTemplate(sqlSessionFactory);}
复制代码

可能有人会对MysqlXADataSource和AtomikosDataSourceBean感到陌生,这其实是JTA分布式事务中用到两个类,在文末会对其展开阐述。如上配置完成了一个数据源的添加,其余数据源按此模板进行复制便可以,但是有一点值得注意,需要将各个DataSource和SqlSessionFactory的Bean名称进行区分并搭配@Qualifier进行选择,不然会导致各个数据源间调用错乱。

动态数据源方案

其实换一个角度想想,我们使用多个SqlSessionFactory来各自连接不同的数据源是很有局限性的。当我们数据源数量比较多的时候类似上文的模板式的代码将充斥整个项目,配置起来比较的繁琐。而且,试想一下,我们并不是每时每刻都对各个数据源都需要进行操作,每个数据源又会保有一个基本的闲置连接数。这样对本就宝贵的系统内存和CPU等资源产生了浪费,所以,第二种方案就应运而生了--动态数据源。我举一个生活中比较形象的例子:工人使用的钻头,其实钻机是只需要一个的,我们只需要根据不同的墙壁材质和孔的形状需要去替换掉钻机上不同的钻头就可以适应各个场景了呀。而上文我们所做的事情是买了两套甚至多套的钻机(真的有点奢侈了!)。来看看该怎么做:

        /*** db_base数据源* @return*/@Bean(name = "base")@ConfigurationProperties(prefix = "spring.datasource.druid.base" )public DataSource base () {return DruidDataSourceBuilder.create().build();}/*** db_b2b数据源* @return*/@Bean(name = "b2b")@ConfigurationProperties(prefix = "spring.datasource.druid.b2b" )public DataSource b2b () {return DruidDataSourceBuilder.create().build();}/*** 动态数据源配置* @return*/@Bean@Primarypublic DataSource multipleDataSource (@Qualifier("base") DataSource base,@Qualifier("b2b") DataSource b2b ) {DynamicDataSource dynamicDataSource = new DynamicDataSource();Map< Object, Object > targetDataSources = new HashMap<>();targetDataSources.put(DBTypeEnum.DB_BASE.getValue(), base );targetDataSources.put(DBTypeEnum.DB_B2B.getValue(), b2b);dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(base);return dynamicDataSource;}@Bean("sqlSessionFactory")public SqlSessionFactory sqlSessionFactory() throws Exception {MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();sqlSessionFactory.setDataSource(multipleDataSource(base(),b2b()));MybatisConfiguration configuration = new MybatisConfiguration();configuration.setJdbcTypeForNull(JdbcType.NULL);configuration.setMapUnderscoreToCamelCase(true);configuration.setCacheEnabled(false);sqlSessionFactory.setConfiguration(configuration);sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()paginationInterceptor() //添加分页功能});sqlSessionFactory.setGlobalConfig(globalConfiguration());return sqlSessionFactory.getObject();}
复制代码

我们现在只需要一个SqlSessionFactory(“钻机”)了,但是多了一个DynamicDataSource。它其实可以看做是一份数据源的清单,后面我们将可以按照清单上的数据源名称进行动态的切换。那么问题又来了,我们怎么知道什么时候该用哪个数据库呢?接着看:

public class DynamicDataSource extends AbstractRoutingDataSource {/*** 核心方法,切换数据源上下文*/@Overrideprotected Object determineCurrentLookupKey() {return DbContextHolder.getDbType();}}public class DbContextHolder {private static final ThreadLocal contextHolder = new ThreadLocal<>();/*** 设置数据源*/public static void setDbType(DBTypeEnum dbTypeEnum) {contextHolder.set(dbTypeEnum.getValue());}/*** 取得当前数据源*/private static String getDbType() {return (String) contextHolder.get();}/*** 清除上下文数据*/private static void clearDbType() {contextHolder.remove();}}@Slf4j
@Aspect
@Order(-100)
@Component
public class DataSourceSwitchAspect {/*** 自己编写的manager method*/@Pointcut("execution(* com.mhc.polestar.dal.manager.*.*(..))")private void ownMethod(){}/*** MP生成的CRUD method*/@Pointcut("execution(* com.baomidou.mybatisplus.service.*.*(..))")private void mpMethod() {}@Before( "ownMethod() || mpMethod()" )public void base(JoinPoint joinPoint) {String name = joinPoint.getTarget().getClass().getName();if (name.contains("B2b")){log.debug("切换到b2b数据源...");DbContextHolder.setDbType(DBTypeEnum.DB_B2B);}else {log.debug("切换到base数据源...");DbContextHolder.setDbType(DBTypeEnum.DB_BASE);}}}
复制代码

还记得面向切面编程的思想么,我们其实每次想去“换钻头”这个动作可以通过切面的方式进行抽象。我们根据切点处所使用的Manager的名称、路径(需要事先制定一些规则)等信息来选择切换到哪个数据源,这样才能正常的工作。同时要通过@Order来保证切面的执行次序优先。说到这里,大体的思想也表达了出来,但是我在实际的使用过程中遇到了一个问题。虽然像上文那样操作可以对代码实现零入侵,但是看看下面这种情况:

        @Override@ValidateDTO@Transactional(rollbackFor = Exception.class)public APIResult<Boolean> addPartner(PartnerParamDTO paramDTO) {...}
复制代码

我们往往为了保证业务一致性需要开启事务,但是如果我们在Manager的调用者Service上开启事务,那么savepoint将被设置在service层,即其下层的上下文环境必须要确定下来不能更改了,这样在发生异常时进行rollBack才能保证一致性。可是,我们的切面设置在了Manger层,这样不就不可以切换数据源,势必会发生错误!后来,我通过自定义注解的方式在Service层就实现把数据源切换好这个问题也就解决,但是这样的方式会对代码产生一定的入侵性(我们需要在所有切换数据源的地方加上注解,这本和原来的业务没有任何关联,而且我们在一个Service方法中需要使用多个数据源,那就需要把这种代码的坏味道进一步深入到Manager层,使用DbContextHolder.setDbType()进行手动切换)。讲到这里,这个我遇见的这个问题也差不多描述结束了。

分布式事务

上文在讲多数据源方案的时候提到了MysqlXADataSource和AtomikosDataSourceBean,这其实是JTA分布式事务中的两个关键。众所周知,传统的Spring事务只能对单个数据源进行一致性管理。现在,我们在项目中使用了多个数据源或者动态数据源,如果我们还想继续使用事务就不得不考虑JTA分布式事务了。它是为了保证多数据源间事务同步,这里我使用atomiko的方式来进行一个简单的演示:

@Configuration
@EnableTransactionManagement
public class TxManagerConfig {@Bean(name = "userTransaction")public UserTransaction userTransaction() throws Throwable {UserTransactionImp userTransactionImp = new UserTransactionImp();userTransactionImp.setTransactionTimeout(10000);return userTransactionImp;}@Bean(name = "atomikosTransactionManager", initMethod = "init" , destroyMethod = "close")public TransactionManager atomikosTransactionManager() {UserTransactionManager userTransactionManager = new UserTransactionManager();userTransactionManager.setForceShutdown(false);return userTransactionManager;}@Bean(name = "transactionManager")@DependsOn({ "userTransaction", "atomikosTransactionManager" })public PlatformTransactionManager transactionManager() throws Throwable {return new JtaTransactionManager(userTransaction(),atomikosTransactionManager());}}/*** db_b2b数据源* @return b2b库数据源*/@Bean(name = "b2b")public DataSource b2b () throws SQLException {MysqlXADataSource mysqlXADataSource=new MysqlXADataSource();mysqlXADataSource.setUrl((dbB2BProperties.getUrl()));mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);mysqlXADataSource.setPassword((dbB2BProperties.getPassword()));mysqlXADataSource.setUser((dbB2BProperties.getUserName()));mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);AtomikosDataSourceBean xaDataSource=new AtomikosDataSourceBean();xaDataSource.setXaDataSource(mysqlXADataSource);xaDataSource.setUniqueResourceName("b2b");return xaDataSource;}
复制代码

你没看错,在SpringBoot项目里面就是如此简单的配置就可以实现分布式事务了。我们使用MysqlXADataSource和AtomikosDataSourceBean对需要管理的数据源进行标识就可以使用JTA提供的PlatformTransactionManager来进行事务的管理。

方案的权衡

1.多数据源方案优势在于配置简单并且对业务代码的入侵性极小,缺点也显而易见:我们需要在系统中占用一些资源,而这些资源并不是一直需要,一定程度上会造成资源的浪费。如果你需要在一段业务代码中同时使用多个数据源的数据又要去考虑操作的原子性(事务),那么这种方案无疑会适合你。
2.动态数据源方案配置上看起来配置会稍微复杂一些,但是很好的符合了“即拿即用,即用即还”的设计原则,我们把多个数据源看成了一个池子,然后进行消费。它的缺点正如上文所暴露的那样:我们往往需要在事务的需求下做出妥协。而且由于需要切换环境上下文,在高并发量的系统上进行资源竞争时容易发生死锁等活跃性问题。我们常用它来进行数据库的“读写分离”,不需要在一段业务中同时操作多个数据源。
3.如果需要使用事务,一定记得使用分布式事务进行Spring自带事务管理的替换,否则将无法进行一致性控制!
写到这里本文也就结束,好久没有撰写文章很多东西考虑不是很详尽,谢谢批评指正!

转载于:https://juejin.im/post/5b790a866fb9a019ea01f38c

多数据源与动态数据源的权衡相关推荐

  1. SpringBoot实现多数据源,动态数据源自由切换

    业务场景 在开发中,可能涉及到在用户的业务中要去查询对应订单的数据,而用户和订单又是分处于不同的数据库的,这样的业务该怎么处理呢? 这种就是多数据源的场景,随着业务量的增大,其实这种情况还是经常能遇到 ...

  2. Spring 下,关于动态数据源的事务问题的探讨

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:青石路 cnblogs.com/youzhibing ...

  3. Spring Boot + Mybatis 实现动态数据源

    动态数据源 在很多具体应用场景的时候,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库.又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动 ...

  4. Spring Boot + Mybatis多数据源和动态数据源配置

    转载自 http://blog.csdn.net/neosmith/article/details/61202084 网上的文章基本上都是只有多数据源或只有动态数据源,而最近的项目需要同时使用两种方式 ...

  5. 分布式数据层中间件详解:如何实现分库分表+动态数据源+读写分离

    分布式数据层中间件: 1.简介: 分布式数据访问层中间件,旨在为供一个通用数据访问层服务,支持MySQL动态数据源.读写分离.分布式唯一主键生成器.分库分表.动态化配置等功能,并且支持从客户端角度对数 ...

  6. SpringBoot(十一)-- 动态数据源

    SpringBoot中使用动态数据源可以实现分布式中的分库技术,比如查询用户 就在用户库中查询,查询订单 就在订单库中查询. 一.配置文件application.properties # 默认数据源 ...

  7. 多数据源/动态数据源的解决方案

    多数据源/动态数据源的解决方案 一. DAO层的解决方案 1.配置数据源 2.创建切点注解 3.AbstractRoutingDataSource 4.读取数据源配置 5.切面拦截 6.使用验证 二. ...

  8. Spring Boot使用动态数据源

    文章目录 前言 一.什么是动态数据源 二.动态数据源实现 1.实现原理 2.实现过程 前言 有这样一个场景,现在要开发一个数据API功能,用户自己编写数据源脚本,在界面任意选择一个数据源,可选择的数据 ...

  9. mysql连接数问题tddl_TDDL动态数据源开源-基本说明-阿里云开发者社区

    总体描述 TDDL动态数据源主要分为2层,每一层都实现了jdbc**规范**,以方便地集成到各种orm框架或者直接使用.每一层都各司其职. TGroupDataSource(tddl group ds ...

最新文章

  1. 程序员必备,新手也可以直接拿来用的jQuery万能代码段
  2. 如何无侵入管理所有的微服务接口?
  3. 网络系统结构和设计的基本规则(Basic rules for network system architecture and design)
  4. yolov5检测完不显示框和标注
  5. QC数据库恢复,解决SQL孤立用户问题
  6. CentOS 迎来更新换代版本 CentOS Stream 9 附下载镜像OS地址
  7. 让我感动的经典台词(zz)
  8. Velocity浅析及与Jsp、Freemarker对比
  9. mvc怎么单独引用css文件,关于asp.net mvc:如何在剃刀视图中引用.css文件?
  10. java代码中出现乱码怎么解决?
  11. java 调度框架_java调度框架Quartz(一)
  12. INSERT INTO SELECT语句概述和示例
  13. Linux 邮件服务器 之跟我一步一步来实现一个邮件系统
  14. SecureCRT下载安装与破解
  15. [转]制作一个64M的U盘启动盘(mini linux + winpe +dos toolbox)
  16. Incorrect string value: '\\xE6\\xBF\\x80\\xE5\\x85\\x89...' for column 'rukuName' at row 1 QMYSQL:
  17. linux系统中的临时文件
  18. html excel 在线编辑,利用js实现在线编辑excel表格代码
  19. 怪物的生成 攻击和掉落金币
  20. 计算机科学与技术统考专业代码,考试类别和级别及专业及科目代码表.doc

热门文章

  1. [51单片机] SPI nRF24L01无线 [可以放在2个单片机里实现通信]
  2. centos7更换源
  3. dfs Codeforces Round #356 (Div. 2) D
  4. 微软:警惕 Web3 和去中心化网络中的“冰钓”威胁
  5. 微软:确实存在另一枚 print spooler 0day,目前尚未修复
  6. Windows PsExec 0day 漏洞获免费微补丁,但仅适用于最新版本
  7. 刚刚修复的Windows 0day和Chrome 0day 已被组合用于 WizardOpium 攻击(详解)
  8. 我是如何从零基础自学到找到工作经过
  9. http状态码竟然可以这样记
  10. 利用lxml爬取豆瓣小组内容文档并保存