在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。

下面列举两种常用的场景:

一种是读写分离的数据源,例如一个读库和一个写库,读库负责各种查询操作,写库负责各种添加、修改、删除。

另一种是多个数据源之间并没有特别明显的操作,只是程序在一个流程中可能需要同时从A数据源和B数据源中取数据或者同时往两个数据库插入数据等操作。

对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在springboot的官网中发现其支持的分布式事务有三种Atomikos 、Bitronix、Narayana。本文涉及内容中使用的分布式事务控制是Atomikos,感兴趣的可以查看https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-jta.html。

当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。

一、了解多数据源配置中的那些坑

其实目前网上已经有许多的关于SpringBoot+Mybatis+druid+Atomikos技术栈的文章,在这里也很感谢那些乐于分享的同行们。本文中涉及的许多的问题也是吸纳了许多中外文相关技术博客文档的优点,算是站在巨人的肩膀做一次总结吧。抛开废话,下面列举一些几点多数据源带来的坑吧。

  1. 配置麻烦,尤其是对于许多开发的新手,看了许多网上的文章,也许还配置不对,还有面对一堆的文章,可能还无法鉴别那些文章的方法是比较可行的。
  2. 配置了多数据源后发现加入事务后并不能完成数据源的切换。
  3. 配置多数据源时发现增加了许多的配置工作量。
  4. springboot环境下mybatis应用打成jar包后无法扫描别名。

二、如何配置一个springboot多数据源项目

本文使用的技术栈是:SpringBoot+Mybatis+druid+Atomikos,因此使用其他技术栈的可以参考他人博客或者是根据本文内容改造。

重要的技术框架依赖:

 <!-- ali druid -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.6</version>
</dependency><!-- mybatis spring -->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.0</version>
</dependency><!--atomikos transaction management-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

注意:对于使用mysql jdbc 6.0的同鞋必须更新druid到最新的1.1.6,否则druid无法支持分布式事务。感兴趣的可查看官方的release说明。

  1. 编写AbstractDataSourceConfig抽象数据源配置
/*** 针对springboot的数据源配置** @author yu on 2017/12/28.*/
public abstract class AbstractDataSourceConfig {protected DataSource getDataSource(Environment env,String prefix,String dataSourceName){Properties prop = build(env,prefix);AtomikosDataSourceBean ds = new AtomikosDataSourceBean();ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");ds.setUniqueResourceName(dataSourceName);ds.setXaProperties(prop);return ds;}protected Properties build(Environment env, String prefix) {Properties prop = new Properties();prop.put("url", env.getProperty(prefix + "url"));prop.put("username", env.getProperty(prefix + "username"));prop.put("password", env.getProperty(prefix + "password"));prop.put("driverClassName", env.getProperty(prefix + "driver-class-name", ""));prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));prop.put("maxPoolPreparedStatementPerConnectionSize",env.getProperty(prefix + "maxPoolPreparedStatementPerConnectionSize", Integer.class));prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));prop.put("validationQueryTimeout", env.getProperty(prefix + "validationQueryTimeout", Integer.class));prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));prop.put("timeBetweenEvictionRunsMillis", env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));prop.put("useGlobalDataSourceStat",env.getProperty(prefix + "useGlobalDataSourceStat", Boolean.class));prop.put("filters", env.getProperty(prefix + "filters"));return prop;}
}

  

ps:AbstractDataSourceConfig对于其他数据库链接池的配置是可以改动的。

2.编写关于基于注解的动态数据源切换代码,这部分主要是将数据库源交给AbstractRoutingDataSource类,并由它的determineCurrentLookupKey()进行决定数据源的选择。关于这部分的代码,其实网上的做法基本差不多,这里也就列举出来了大家可以阅读其他相关的博客,但是这部分的代码是可以单独封装成一个模块的,封装好后不管对于Springboot项目还是SpringMVC项目将封装的模块导入都是可以正常工作的。可以参考本人目前开源的https://gitee.com/sunyurepository/ApplicationPower项目中的datasource-aspect模块。

3.应用2中的通用封装模块并做写小改动,这里所谓的主要是你可能会像,在上面第二步中的写的切面作用类可能没有是用aop的注解或者是使用自定义注解的默认拦截失效,这时继承下通用模块中的类重写一个AOP作用类。例如:

@Aspect
@Component
public class DbAspect extends DataSourceAspect {@Pointcut("execution(* com.power.learn.dao.*.*(..))")@Overrideprotected void datasourceAspect() {super.datasourceAspect();}
}

  

4.编写一个MyBatisConfig,该类的作用就是创建Mybatis多个数据源的java配置了。例如想建立两个数据源一个叫one,另一个叫two

@Configuration
@MapperScan(basePackages = MyBatisConfig.BASE_PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisConfig extends AbstractDataSourceConfig {//mapper模式下的接口层static final String BASE_PACKAGE = "com.power.learn.dao";//对接数据库的实体层static final String ALIASES_PACKAGE = "com.power.learn.model";static final String MAPPER_LOCATION = "classpath:com/power/learn/mapping/*.xml";@Primary@Bean(name = "dataSourceOne")public DataSource dataSourceOne(Environment env) {String prefix = "spring.datasource.druid.one.";return getDataSource(env,prefix,"one");}@Bean(name = "dataSourceTwo")public DataSource dataSourceTwo(Environment env) {String prefix = "spring.datasource.druid.two.";return getDataSource(env,prefix,"two");}@Bean("dynamicDataSource")public DynamicDataSource dynamicDataSource(@Qualifier("dataSourceOne")DataSource dataSourceOne,@Qualifier("dataSourceTwo")DataSource dataSourceTwo) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("one",dataSourceOne);targetDataSources.put("two",dataSourceTwo);DynamicDataSource dataSource = new DynamicDataSource();dataSource.setTargetDataSources(targetDataSources);dataSource.setDefaultTargetDataSource(dataSourceOne);return dataSource;}@Bean(name = "sqlSessionFactoryOne")public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("dataSourceOne") DataSource dataSource)throws Exception {return createSqlSessionFactory(dataSource);}@Bean(name = "sqlSessionFactoryTwo")public SqlSessionFactory sqlSessionFactoryTwo(@Qualifier("dataSourceTwo") DataSource dataSource)throws Exception {return createSqlSessionFactory(dataSource);}@Bean(name = "sqlSessionTemplate")public CustomSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryOne")SqlSessionFactory factoryOne,@Qualifier("sqlSessionFactoryTwo")SqlSessionFactory factoryTwo) throws Exception {Map<Object,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();sqlSessionFactoryMap.put("one",factoryOne);sqlSessionFactoryMap.put("two",factoryTwo);CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(factoryOne);customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);return customSqlSessionTemplate;}/*** 创建数据源* @param dataSource* @return*/private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception{SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setVfs(SpringBootVFS.class);bean.setTypeAliasesPackage(ALIASES_PACKAGE);bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));return bean.getObject();}
}

  

划重点(考试要考):注意最后createSqlSessionFactory方法中的这一行代码bean.setVfs(SpringBootVFS.class),对于springboot项目采用java类配置Mybatis的数据源时,mybatis本身的核心库在springboot打包成jar后有个bug,无法完成别名的扫描,在低版本的mybatis-spring-boot-starter中需要自己继承Mybatis核心库中的VFS重写它原有的资源加载方式。在高版本的mybatis-spring-boot-starter已经帮助实现了一个叫SpringBootVFS的类。感兴趣的可以到官方项目了解这个bughttps://github.com/mybatis/spring-boot-starter/issues/177。

5.解决分布式事务控制下数据源无法动态切换的问题。对于为每一个数据源创建单独的静态数据源并且配置固定以扫描不同包上的mapper接口层情况是不会出现这种问题的,可以很好的调用不同包下的mapper层,因为数据源一开就已经初始化好了,分布式事务不会影响你调用不同的数据源,也不需要前面的步骤。

对于动态多数据源架构的场景,数据源都是通过aop来完成切换了,但是因为事务控制在切换之前,因此切换就被事务阻止了。曾经在解决这个问题是,很幸运的是我在google中搜索是发现了一个很有趣的方案,并且是国内的人实现放在github上的。下面看下源码核心。

*** from https://github.com/igool/spring-jta-mybatis*/
public class CustomSqlSessionTemplate extends SqlSessionTemplate {//......省略@Overridepublic SqlSessionFactory getSqlSessionFactory() {SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());if (targetSqlSessionFactory != null) {return targetSqlSessionFactory;} else if (defaultTargetSqlSessionFactory != null) {return defaultTargetSqlSessionFactory;} else {Assert.notNull(targetSqlSessionFactorys, "Property 'targetSqlSessionFactorys' or 'defaultTargetSqlSessionFactory' are required");Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactorys' are required");}return this.sqlSessionFactory;}//......省略}

  

就是重写一个SqlSessionTemplate来改变让SqlSessionFactory动态的获取数据源。

targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());

  

DataSourceContextHolder一般就是你在第二步中创建的数据源上下文操作类,这个只需要根据自己需求做改动即可。当然这个类我个人也建议像第二步一样单独放到一个模块中,可以参考本人目前开源的https://gitee.com/sunyurepository/ApplicationPower项目中的mybatis-template模块。专门为mybatis场景准备,但是我不建议和第二步和代码合并在一起,因为对于数据切换的切面控制代码可以放到非mybatis的项目中。

6.多数据源的项目配置文件配置。这里采用yml。其配置参考如下:

#Spring boot application.yml# spring
spring:#profiles : devdatasource:type: com.alibaba.druid.pool.DruidDataSourcedruid:one:url: jdbc:mysql://localhost:3306/project_boot?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.DriverminIdle: 1maxActive: 20initialSize: 1timeBetweenEvictionRunsMillis: 3000minEvictableIdleTimeMillis: 300000validationQuery: SELECT 'ZTM' FROM DUALvalidationQueryTimeout: 10000testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsemaxWait: 60000# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20filters: stat,wall,log4j2useGlobalDataSourceStat: truetwo:url: jdbc:mysql://localhost:3306/springlearn?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.DriverminIdle: 1maxActive: 20initialSize: 1timeBetweenEvictionRunsMillis: 3000minEvictableIdleTimeMillis: 300000validationQuery: SELECT 'ZTM' FROM DUALvalidationQueryTimeout: 10000testWhileIdle: truetestOnBorrow: falsetestOnReturn: falsemaxWait: 60000# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20filters: stat,wall,log4j2useGlobalDataSourceStat: truejta:atomikos:properties:log-base-dir: ../logstransaction-manager-id: txManager
server:port: 8080undertow:accesslog:enabled: truedir: ../logs

ps:jta就是配置让springboot启动分布式事务支持。

7.编码测试

dao层实例(对应两个数据源,使用注解动态切换):

@TargetDataSource(DataSourceKey.ONE)
public interface StudentOneDao {/*** 保存数据* @param entity* @return*/int save(Student entity);}@TargetDataSource(DataSourceKey.TWO)
public interface StudentTwoDao {/*** 保存数据* @param entity* @return*/int save(Student entity);
}

  

service层

@Service("studentOneService")
public class StudentOneServiceImpl implements StudentService {/*** 日志*/private Logger logger = LoggerFactory.getLogger(this.getClass());@Resourceprivate StudentOneDao studentOneDao;@Resourceprivate StudentTwoDao studentTwoDao;@Transactional@Overridepublic CommonResult save(Student entity) {CommonResult result = new CommonResult();try {studentOneDao.save(entity);studentTwoDao.save(entity);int a = 10/0;result.setSuccess(true);} catch (Exception e) {logger.error("StudentService添加数据异常:",e);//抛出异常让异常restful化处理throw new RuntimeException("添加数据失败");}return result;}
}

  

ps:除0操作强行造一个异常来检测分布式事务是否生效,注意对于自己捕获处理的异常情况需要throw出去,否则事务不会生效的。可以参考我提供的demo https://gitee.com/sunyurepository/multiple-datasource

三、如何解决这些该死的配置?

按照上面的步骤处理后,基本就完成了一个多数据源应用的基础架构了,但是有人会发现了,上面这么多的配置,搞这么多代码,几分钟的时间能搞定吗,答案基本不太可能,一不小心可能还会因为写错了数据源名称又搞半天。

因此我将介绍一种真正用几分钟时间来搭建一个多数据源项目的方法。帮你省掉这些重复的配置工作,轻松玩转n个数据源,抛弃那些该死的配置,分分钟创建一个demo。

第一步:下载https://gitee.com/sunyurepository/ApplicationPower项目

第二步:将Common-util、datasource-aspect、mybatis-template三个模块安装到你的本地maven仓库中。对于idea的用户只需要点3下大家都懂得,eclipse的用户默默的抹下眼泪吧。

第三步:在application-power的resources下找到jdbc.properties连接一个mysql的数据库.

第四步:在application-power的resources下找到generator.properties修改按照说明修改就好了

# @since 1.5
# 打包springboot时是否采用assembly
# 如果采用则将生成一系列的相关配置和一系列的部署脚本
generator.package.assembly=true#@since 1.6
# 多数据源多个数据数据源用逗号隔开,不需要多数据源环境则空出来
# 对于多数据源会集成分布式事务
generator.multiple.datasource=mysql,oracle# @since 1.6
# jta-atomikos分布式事务支持
generator.jta=true

  

主要的就是制定自己想取的数据源名称吧,如上我一个连接mysql,一个连接oracle。其他的根据自己的需求来改。

第五步:运行application-power的test中的

GenerateCodeTest

完成所有项目代码的产生和输出,然后你就可以导入idea工具测试了。

创建完你要做的几件事:

  1. 自动创建的项目会在dao层默认注入你配置的第一个数据源,因此需要根据自己的情况修改,关于数据源的名称已经自动帮你创建了一个常量类中。
  2. 给service的方法需要使用事务的方法自己加事务注解
  3. 对于非mysql数据库你需要自己添加驱动包,创建的代码默认添加mysql驱动包
  4. 在application.yml中修改你的数据源用户名,密码和连接的url地址,因为生成的默认是copy你连接数据库生成项目时的数据库连接信息。

小结:其实创建完后整个工作就是做极少的修改,多数据源的所有配置都创建好了,连两个和连5个数据源带来的工作并不大。当然如果想用ApplicationPower来创建真实应用的童鞋,如果觉得模板中的一些依赖模块不想在公司使用也是可以稍微修改小模板来从新生成的,在使用中也希望有更好的建议被提出。

总结:

本文主要只是对许多多数据源场景使用中相关优秀文章的总结。我个人仅仅是将这些总结的东西通过封装和我个人开源放在码云上的ApplicationPower脚手架将SpringBoot+Mybatis+druid+Atomikos的多数据源和分布式事务架构的配置通过自动化来快速输出。

申明:转载本博客内容请注明原地址https://my.oschina.net/u/1760791/blog/1605367

参考博客:

http://blog.csdn.net/a510835147/article/details/75675311等

转载于:https://www.cnblogs.com/telwanggs/p/10875366.html

3分钟搞定SpringBoot+Mybatis+druid多数据源和分布式事务相关推荐

  1. 一篇搞定 SpringBoot+Mybatis+Shiro 实现多角色权限管理

    初衷:我在网上想找整合springboot+mybatis+shiro并且多角色认证的博客,发现找了好久也没有找到想到的,现在自己会了,就打算写个博客分享出去,希望能帮到你. 原创不易,请点赞支持! ...

  2. SpringBoot+MyBatis(动态数据源/分布式事务XA(Atomikos))

    快速集成工具,欢迎打脸 说明 适用环境:SpringBoot / MyBatis / Atomickos  特性: 多数据源,动态切换 多数据源,XA分布式事务支持(需引入Atomickos) 仅支持 ...

  3. 基于脚手架 3 分钟搞定 SpringBoot 后端管理项目

    知道的越多,不知道的就越多,业余的像一棵小草! 编辑:业余草 来源:https://www.xttblog.com/?p=4988 题外话: 前几天,有不少人问我,有没有基于 SpringBoot 的 ...

  4. 10分钟搞定 SpringBoot 如何优雅读取配置文件?

    本文以及收录自springboot-guide(不只是SpringBoot还有Spring重要知识点),地址:https://github.com/Snailclimb/springboot-guid ...

  5. Springboot+Mybatis+Druid+Maven多模块项目搭建遇到的各种吭

    Springboot+Mybatis+Druid+Maven多模块项目搭建 这里记录一下搭建多模块遇到的吭 首先建立一个父级空项目,在pox里修改下配置 2,建立DaoMapper层和ModelEnt ...

  6. java 分组报表_【Java】分组报表怎么做,积木报表十分钟搞定!

    首页 专栏 java 文章详情 0 分组报表怎么做,积木报表十分钟搞定! scott发布于 今天 12:24 报表需求 某大型超市需要做一张年度区域销售统计报表 展示2019和2020年度各地区每月的 ...

  7. 三步10分钟搞定数据库版本的降迁 (将后台数据库SQL2008R2降为SQL2005版本)

    三步10分钟搞定数据库版本的降迁 (将SQL2008R2降为SQL2005版本) 转载原文,并注明出处!虽无多少技术含量,毕竟是作者心血原创,希望理解. 转自 http://blog.csdn.net ...

  8. 启程 - 《每日五分钟搞定大数据》

    <每日五分钟搞定大数据>原创系列,每周不定期更新.欢迎关注公众号:大叔据   想了很久,准备开始写一系列的文章,记录这些年来的所得所想,感觉内容比较多不知从哪里开始,画了个思维导图确定了大 ...

  9. python使用教程pandas-十分钟搞定pandas(入门教程)

    本文是对pandas官方网站上<10Minutes to pandas>的一个简单的翻译,原文在这里.这篇文章是对pandas的一个简单的介绍,详细的介绍请参考:Cookbook .习惯上 ...

最新文章

  1. 十进制转化为十六进制分割高低位
  2. Java多线程--synchronized修饰普通方法和修饰静态方法的区别
  3. 【Alljoyn】 Alljoyn学习笔记七 Alljoyn瘦客户端库介绍
  4. C++ 语言基础 —— STL —— 算法 —— 二分查找算法
  5. linux账号和权限管理思维导图,Linux系统支持的思维导图软件有哪些?
  6. javascript基础-ajax
  7. flask pyecharts_利用 Flask 动态展示 Pyecharts 图表数据的几种方法
  8. c语言中感叹号什么意思_啥是c语言-c语言感叹号用法-c语言中的/和%表示什么意思...
  9. openwrt修改lan口地址失败_OpenWrt刷机后LAN口无法连通的问题
  10. java实现高德地图app_入门指南-高德地图手机版 | 高德地图API
  11. 西瓜视频解析原理及源码,使用CRC32的签名算法,获得视频源地址
  12. 总体设计、概要设计和详细设计
  13. mysql 批量查询
  14. 【程序源代码】小程序商城系统(CoreShop)
  15. win10推送_Win10新版19631推送,网友:千万别翻车
  16. 嵌入式学习班到底怎么样?
  17. 【找出耗电大户进行消灭 电池持久有绝招】
  18. java接口废弃注释_Spring Boot如何让Web API自动生成文档,并解决swagger-annotations的API注解description属性废弃的问题...
  19. 【破文标题】久其2005年度部门决算报表软件绿化
  20. 佳博便携式条码打印机怎么使用_佳博打印机如何设置热敏打印

热门文章

  1. 一直跳出来 visual_六天时间排出来肾结石,这种经历再也不要了
  2. (38)时钟延迟约束
  3. (69)FPGA面试题-使用不同的代码实现2:1 MUX ?使用if语句
  4. (60)FPGA比较器实现(function)
  5. 1004.串口收发数据集成bug
  6. Nginx基本数据结构之ngx_list_t
  7. 创建一个路由节点struct fib_node
  8. Linux内核开发之异步通知与异步I/O《来自linux设备开发详解》
  9. Linux中sudo命令设置,Linux下sudo命令的配置与使用方法
  10. C语言表达式的求解规则,C语言实现整数四则运算表达式的计算