文章來自: https://blog.csdn.net/qq_29242877/article/details/79033287
在一些復雜的應用開發中,一個應用可能會涉及到連接多個數據源,所謂多數據源這里就定義為至少連接兩個及以上的數據庫了。
下面列舉兩種常用的場景:
一種是讀寫分離的數據源,例如一個讀庫和一個寫庫,讀庫負責各種查詢操作,寫庫負責各種添加、修改、刪除。
另一種是多個數據源之間並沒有特別明顯的操作,只是程序在一個流程中可能需要同時從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技術棧的文章,在這里也很感謝那些樂於分享的同行們。本文中涉及的許多的問題也是吸納了許多中外文相關技術博客文檔的優點,算是站在巨人的肩膀做一次總結吧。拋開廢話,下面列舉一些幾點多數據源帶來的坑吧。
- 配置麻煩,尤其是對於許多開發的新手,看了許多網上的文章,也許還配置不對,還有面對一堆的文章,可能還無法鑒別那些文章的方法是比較可行的。
- 配置了多數據源后發現加入事務后並不能完成數據源的切換。
- 配置多數據源時發現增加了許多的配置工作量。
- 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說明。
- 編寫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; } /** * 主要針對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工具測試了。
創建完你要做的幾件事:
- 自動創建的項目會在dao層默認注入你配置的第一個數據源,因此需要根據自己的情況修改,關於數據源的名稱已經自動幫你創建了一個常量類中。
- 給service的方法需要使用事務的方法自己加事務注解
- 對於非mysql數據庫你需要自己添加驅動包,創建的代碼默認添加mysql驅動包
- 在application.yml中修改你的數據源用戶名,密碼和連接的url地址,因為生成的默認是copy你連接數據庫生成項目時的數據庫連接信息。
小結:其實創建完后整個工作就是做極少的修改,多數據源的所有配置都創建好了,連兩個和連5個數據源帶來的工作並不大。當然如果想用ApplicationPower來創建真實應用的童鞋,如果覺得模板中的一些依賴模塊不想在公司使用也是可以稍微修改小模板來從新生成的,在使用中也希望有更好的建議被提出。
總結:
本文主要只是對許多多數據源場景使用中相關優秀文章的總結。我個人僅僅是將這些總結的東西通過封裝和我個人開源放在碼雲上的ApplicationPower腳手架將SpringBoot+Mybatis+druid+Atomikos的多數據源和分布式事務架構的配置通過自動化來快速輸出。
