在一些複雜的應用開發中,一個應用可能會涉及到鏈接多個數據源,所謂多數據源這裏就定義爲至少鏈接兩個及以上的數據庫了。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,所以使用其餘技術棧的能夠參考他人博客或者是根據本文內容改造。數據庫
重要的技術框架依賴:springboot
<!-- 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說明。restful
/** * 針對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.*.*(..))") @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 boot application.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: 20 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 useGlobalDataSourceStat: true 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: 20 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 useGlobalDataSourceStat: true jta: atomikos: properties: log-base-dir: ../logs transaction-manager-id: txManager server: port: 8080 undertow: accesslog: enabled: true dir: ../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()); @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的多數據源和分佈式事務架構的配置經過自動化來快速輸出。
申明:轉載本博客內容請註明原地址http://www.javashuo.com/article/p-ucjreklo-dx.html
參考博客:
http://blog.csdn.net/a510835147/article/details/75675311等