javascript
谈一谈Spring-Mybatis在多数据源配置上的坑
- 蘇格團隊
- 作者:JayceKon
- 交流QQ群:855833773
- 歡迎加入我們的團隊,微信聯系方式:foreverpx_cjl
概述
先聊一聊業務背景,隨著系統服務的不斷開發,我們的系統會充斥著各種個樣的業務.這種時候,我們應該要開始考慮一下如何將系統的粒度細化.舉個常見的例子: 電商系統可以拆分為 商品模塊,訂單模塊,地址模塊等等.這些模塊都可以獨立抽取出來,形成一個單獨的服務.這就會涉及到各個模塊之間的通信問題,一些簡單的服務,我們可以通過 rpc 接口 直接進行通信,但是有些服務卻不適用這種模式.本文主要講一下在多數據源路上遇到的一些坑.
多數據源
項目結構
源碼地址: github.com/jaycekon/Sp…
配置文件: DataSourceConfig
(name = "masterDataSource")("masterDataSource")(prefix = "spring.datasource")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}(name = "slaveDataSource")("slaveDataSource")(prefix = "spring.datasource.db2")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,@Qualifier("slaveDataSource") DataSource slave) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DatabaseType.db1, master);targetDataSources.put(DatabaseType.db2, slave);DynamicDataSource dataSource = new DynamicDataSource();dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法dataSource.setDefaultTargetDataSource(master);// 默認的datasource設置為myTestDbDataSourcereturn dataSource;}public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource,@Qualifier("slaveDataSource") DataSource myTestDb2DataSource) throws Exception {SqlSessionFactoryBean fb = new SqlSessionFactoryBean();fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource));fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));return fb.getObject();} 復制代碼項目創建流程可以參: 《Spring-Mybatis 讀寫分離》
數據庫
test_1:
CREATE TABLE `school` (`id` int(11) NOT NULL AUTO_INCREMENT,`school_name` varchar(255) DEFAULT NULL,`province` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; 復制代碼test_2:
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(255) DEFAULT NULL,`password` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 復制代碼1、數據庫鏈接異常
此數據庫鏈接異常,指的是在 切換數據源 時,數據庫鏈接異常
啟動我們的服務:
說明我們的服務配置是沒有什么問題的,那么所謂的數據庫鏈接異常又是什么回事呢?
Test:
private SchoolService schoolService;private UserService userService;public void addUser() {userService.inserUser("root2","root2");}public void addSchool() {schoolService.addSchool("ceshi1", "ceshi1");} 復制代碼通過注解設置數據源:
@Service @DataSource("db2") public class UserService@Service @DataSource("db1") public class SchoolService 復制代碼我們創建了一個測試類,來檢測兩個數據源處理情況
從結果來看:
1、schoolService 成功了 (db:test_1)
2、UserService 失敗了( db:test_2)
errorMessage:
org.springframework.jdbc.BadSqlGrammarException: ### Error updating database. Cause: java.sql.SQLSyntaxErrorException: Table 'test_1.user' doesn't exist ### The error may involve com.jaycekon.mybatis.multi.mapper.UserMapper.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO `user`(`username`, `password`) VALUES ( ?, ?); ### Cause: java.sql.SQLSyntaxErrorException: Table 'test_1.user' doesn't exist ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'test_1.user' doesn't exist 復制代碼上述異常,即我們可能會遇到的第一個坑: UserService 中的數據源鏈接異常
異常分析
1、數據源鏈接的是 test_1 說明沒有成功切換數據源
2、觀察切面方法,監聽的是 dataSource
@Before("@annotation(com.jaycekon.mybatis.multi.config.DataSource)") 復制代碼3、@DataSource
@Retention(RetentionPolicy.CLASS) @Target({ElementType.TYPE}) public @interface DataSource 復制代碼通過上述注解可以發現,我們注解對象為 TYPE(類),而在 AspectJ 中的注解監聽,只支持方法注解監聽,并不能監聽類的注解.因此,在上述我們通過注解整個類的方式,并不能做到數據源動態切換:
@Service @DataSource("db2") public class UserService@Service @DataSource("db1") public class SchoolService 復制代碼解決辦法
1、修改 DataSource 為方法注解,對每個需要切換數據源的方法進行監聽.該方法 比較蠢.
2、通過@Pointcut("execution(* com.jaycekon.demo.mapper.*.*(..))") 通過Pointcut 的形式,可以監聽到某個包下面的所有類,所有方法.這個方法還行,但是每次如果創建了新的類,有可能需要修改配置.
3、目前采用的方式為,將不同數據源的mapper,type-aliases,config 分開 配置方式可參考: 傳送門
修改后目錄(配置文件只需保留兩項即可):
2、Mapper 映射異常
在我們修改新的配置文件后,可以參考下面代碼(db2 類似):
@Configuration @MapperScan(value = "com.jaycekon.mybatis.multi.mapper.db1") @EnableTransactionManagement public class DataSourceConfig {private static final String MAPPER_LOCATION = "mybatis.mapper-locations.db1";@Autowiredprivate Environment env;@Bean(name = "masterDataSource")@Qualifier("masterDataSource")@ConfigurationProperties(prefix = "spring.datasource")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "db1SqlSessionFactory")@Primarypublic SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource) throws Exception {SqlSessionFactoryBean fb = new SqlSessionFactoryBean();fb.setDataSource(myTestDbDataSource);fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty(MAPPER_LOCATION)));return fb.getObject();}@Beanpublic DataSourceTransactionManager transactionManager(@Qualifier("masterDataSource") DataSource myTestDbDataSource) {return new DataSourceTransactionManager(myTestDbDataSource);} } 復制代碼其實這里的配置文件隱藏了一個坑,在我們啟動編譯時,并不會出現什么問題,但是當我們訪問 (db2) 的時候,問題就來了:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.jaycekon.mybatis.multi.mapper.db2.UserMapper.insert 復制代碼我們可以看到,db1(school) 的單元測試沒有問題,但是 db2(user) 卻出了問題.
異常分析
1、Mapper 掃描沒有找到對應的 XML 文件
2、多數據源存在多個 SqlSessionFactory ,需要將 Mapper文件綁定到對應的 SqlSessionFactory
3、解決辦法,在掃描 Mapper 時,將其綁定到對應的 SqlSessionFactory :
@MapperScan(value = "com.jaycekon.mybatis.multi.mapper.db2", sqlSessionFactoryRef = "db2SqlSessionFactory") 復制代碼在 @MapperScan 中可以看到對應的解釋:
* Specifies which {@code SqlSessionFactory} to use in the case that there is* more than one in the spring context. Usually this is only needed when you* have more than one datasource. 復制代碼啟動測試類--pass ,啟動程序-- pass
如果你覺得這個坑到這里就結束了,你就太小看我了~
2.1 TypeAliases 映射
正常來說,我們單元測試 & 服務都沒有問題,講道理是能夠正常進行接下來的開發了.但是,我們如果使用的是 Spring-Boot 進行開發,那我們在發布前就還需要做一個操作 打包 Jar包 ,隨后用命令行啟動服務:
java -jar target/spring-boot-mybatis-multi.jar
And Then,然后就會出現下述問題:
Failed to parse mapping resource: 'class path resource [mybatis-mappers/db2/UserMapper.xml]'; nested exception is org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. Cause: org.apache.ibatis.builder.BuilderException: Error resolving class. Cause: org.apache.ibatis.type.TypeException: Could not resolve type alias 'User'. Cause: java.lang.ClassNotFoundException: Cannot find class: User復制代碼在配置 SqlSessionFactory 我們已經設置了 TypeAliasesPackage 的掃描路徑:
@Bean(name = "db1SqlSessionFactory")@Primarypublic SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource) throws Exception {...fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));...} 復制代碼但是他并沒有起任何作用,這是為什么呢?
異常分析
1、別名掃描沒有起作用
2、到Github 查找相關內容,會發現有相同的經歷: 傳送門
解決辦法
1、不使用別名(不是個好辦法)
2、在mybatis/spring-boot-starter 這個項目中,提出了一個官方的 Demo
我們截取中間比較關鍵的一部分代碼:
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();factory.setDataSource(dataSource);factory.setVfs(SpringBootVFS.class); 復制代碼我們采用方法2 嘗試一下,看看能不能解決問題:
關于 VFS 的一些解釋: 虛擬文件系統(VFS),用來讀取服務器里的資源 復制代碼個人理解為,新創建的 SqlSessionFactory 沒有能夠加載配置文件,導致除 @Primary 外的所有 SqlSessionFactory 都沒辦法加載相關配置文件.
3、Config 異常
一路配置下來,單元測試跑通了,服務啟動也成功了,接下來就是一頓騷操作,各種功能開發~ 在開發完成后,進入測試階段.一看數據返回,坑爹啊~~
怎么返回了個空數據?
異常分析
1、數據有返回,服務沒有問題
2、schoolName 對應 數據庫 school_name,中間轉換需要使用駝峰命名轉換
駝峰命名轉換 mybatis.configuration.map-underscore-to-camel-case 出問題了.
解決辦法
1、添加配置 mybatis.configuration.map-underscore-to-camel-case=true
2、創建 MybatisConfig 配置類(db2 類似):
@Bean@ConfigurationProperties(prefix = "mybatis.configuration")@Scope("prototype")public org.apache.ibatis.session.Configuration globalConfiguration() {return new org.apache.ibatis.session.Configuration();}@Bean(name = "db1SqlSessionFactory")@Primarypublic SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource,org.apache.ibatis.session.Configuration config) throws Exception {...fb.setConfiguration(config);...} 復制代碼3、@Scope("prototype") 這里配置類使用的是多實例作用域,主要是為了解決單例模式會影響到數據源的鏈接.
數據庫連接超時
當你屁顛屁顛的把項目發布到服務器,接口調試都沒有問題.過了一晚突然發現,服務掛了,what happen?
{"msg": "\n### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.\n### SQL: ******\n###Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.\n; SQL [];No operations allowed after connection closed.; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.","code": 500 } 復制代碼MySQL5.0以后針對超長時間DB連接做了一個處理,如果一個DB連接在無任何操作情況下過了8個小時后(Mysql 服務器默認的“wait_timeout”是8小時),Mysql會自動把這個連接關閉。這就是問題的所在,在連接池中的connections如果空閑超過8小時,mysql將其斷開,而連接池自己并不知道該connection已經失效,如果這時有 Client請求connection,連接池將該失效的Connection提供給Client,將會造成上面的異常。 所以配置datasource時需要配置相應的連接池參數,定時去檢查連接的有效性,定時清理無效的連接。引用
解決辦法-完善相關配置:
spring.datasource.jdbcUrl=jdbc:mysql://localhost:3306/test_1 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.default-auto-commit = false spring.datasource.default-read-only = true spring.datasource.max-idle = 10 spring.datasource.max-wait = 10000 spring.datasource.min-idle = 5 spring.datasource.initial-size = 5 spring.datasource.validation-query = SELECT 1 spring.datasource.test-on-borrow = false spring.datasource.test-while-idle = true spring.datasource.time-between-eviction-runs-millis = 18800spring.datasource.db2.jdbcUrl=jdbc:mysql://localhost:3306/test_2 spring.datasource.db2.username=root spring.datasource.db2.password=123456 spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver spring.datasource.db2.default-auto-commit = false spring.datasource.db2.default-read-only = true spring.datasource.db2.max-idle = 10 spring.datasource.db2.max-wait = 10000 spring.datasource.db2.min-idle = 5 spring.datasource.db2.initial-size = 5 spring.datasource.db2.validation-query = SELECT 1 spring.datasource.db2.test-on-borrow = false spring.datasource.db2.test-while-idle = true spring.datasource.db2.time-between-eviction-runs-millis = 18800復制代碼4、事務異常
由于我們在多數據源中,采用了多 sqlSessionFactory 方式,因此在事務管理這塊,會出現事務管理異常相關問題,有興趣的童鞋可以參考:www.atomikos.com/Main/WebHom… ,推薦一個整合的 Demo
總結
Mybatis 多數據源配置主要分兩種,一種動態配置數據源 & 一種配置多 sqlsessionFactory,本文的一些坑,主要基于 多 sqlSessionFactory. 上述的所有問題,都是在開發過程中所遇到,可能各位或多或少有遇到過,希望能給各位相關幫助.
如對個人見解有所異議,歡迎指正.
Demo地址: github.com/jaycekon/Sp…
總結
以上是生活随笔為你收集整理的谈一谈Spring-Mybatis在多数据源配置上的坑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 廖大python实战项目第三天
- 下一篇: SpringBoot+Shiro学习(一