文章来自: https://blog.csdn.net/qq_29242877/article/details/79033287 css
在一些复杂的应用开发中,一个应用可能会涉及到链接多个数据源,所谓多数据源这里就定义为至少链接两个及以上的数据库了。html
下面列举两种经常使用的场景:java
一种是读写分离的数据源,例如一个读库和一个写库,读库负责各类查询操做,写库负责各类添加、修改、删除。mysql
另外一种是多个数据源之间并无特别明显的操做,只是程序在一个流程中可能须要同时从A数据源和B数据源中取数据或者同时往两个数据库插入数据等操做。git
对于这种多数据的应用中,数据源就是一种典型的分布式场景,所以系统在多个数据源间的数据操做必须作好事务控制。在springboot的官网中发现其支持的分布式事务有三种Atomikos 、Bitronix、Narayana。本文涉及内容中使用的分布式事务控制是Atomikos,感兴趣的能够查看https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-jta.html。github
固然分布式事务的做用并不只仅应用于多数据源。例如:在作数据插入的时候往一个kafka消息队列写消息,若是信息很重要一样须要保证分布式数据的一致性。spring
其实目前网上已经有许多的关于SpringBoot+Mybatis+druid+Atomikos技术栈的文章,在这里也很感谢那些乐于分享的同行们。本文中涉及的许多的问题也是吸纳了许多中外文相关技术博客文档的优势,算是站在巨人的肩膀作一次总结吧。抛开废话,下面列举一些几点多数据源带来的坑吧。sql
本文使用的技术栈是:SpringBoot+Mybatis+druid+Atomikos,所以使用其余技术栈的能够参考他人博客或者是根据本文内容改造。数据库
重要的技术框架依赖:swift
<!-- 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说明。
/** * 针对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; } /** * 主要针对druid数据库连接池 * @param env * @param prefix * @return */ 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 + "driverClassName", "")); 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("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("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.*.*(..))") @Override protected 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 { //......省略 @Override public 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 spring: #profiles : dev datasource: type: com.alibaba.druid.pool.DruidDataSource druid: one: url: jdbc:mysql://localhost:3306/project_boot?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver minIdle: 1 maxActive: 2 initialSize: 1 timeBetweenEvictionRunsMillis: 3000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 'ZTM' FROM DUAL validationQueryTimeout: 10000 testWhileIdle: true testOnBorrow: false testOnReturn: false maxWait: 60000 # 打开PSCache,而且指定每一个链接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall,log4j2 two: url: jdbc:mysql://localhost:3306/springlearn?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver minIdle: 1 maxActive: 2 initialSize: 1 timeBetweenEvictionRunsMillis: 3000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 'ZTM' FROM DUAL validationQueryTimeout: 10000 testWhileIdle: true testOnBorrow: false testOnReturn: false maxWait: 60000 # 打开PSCache,而且指定每一个链接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall,log4j2 jta: atomikos: properties: log-base-dir: ../logs transaction-manager-id: txManager
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()); @Resource private StudentOneDao studentOneDao; @Resource private StudentTwoDao studentTwoDao; @Transactional @Override public 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工具测试了。
建立完你要作的几件事:
小结:其实建立完后整个工做就是作极少的修改,多数据源的全部配置都建立好了,连两个和连5个数据源带来的工做并不大。固然若是想用ApplicationPower来建立真实应用的童鞋,若是以为模板中的一些依赖模块不想在公司使用也是能够稍微修改小模板来重新生成的,在使用中也但愿有更好的建议被提出。
本文主要只是对许多多数据源场景使用中相关优秀文章的总结。我我的仅仅是将这些总结的东西经过封装和我我的开源放在码云上的ApplicationPower脚手架将SpringBoot+Mybatis+druid+Atomikos的多数据源和分布式事务架构的配置经过自动化来快速输出。