談一談Spring-Mybatis在多資料來源配置上的坑
先聊一聊業務背景,隨著系統服務的不斷開發,我們的系統會充斥著各種個樣的業務.這種時候,我們應該要開始考慮一下如何將系統的粒度細化.舉個常見的例子: 電商系統可以拆分為 商品模組,訂單模組,地址模組等等.這些模組都可以獨立抽取出來,形成一個單獨的服務.這就會涉及到各個模組之間的通訊問題,一些簡單的服務,我們可以通過 rpc
介面 直接進行通訊,但是有些服務卻不適用這種模式.本文主要講一下在 多資料來源
路上遇到的一些坑.
多資料來源

專案結構
原始碼地址: SpringBoot%2Ftree%2Fmaster%2Fspring-boot-mybatis-multi" rel="nofollow,noindex">github.com/jaycekon/Sp…

配置檔案: DataSourceConfig
@Bean(name = "masterDataSource") @Qualifier("masterDataSource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "slaveDataSource") @Qualifier("slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary 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設定為myTestDbDataSource return dataSource; } @Bean 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:
@Autowired private SchoolService schoolService; @Autowired private UserService userService; @Test public void addUser() { userService.inserUser("root2","root2"); } @Test 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"; @Autowired private Environment env; @Bean(name = "masterDataSource") @Qualifier("masterDataSource") @ConfigurationProperties(prefix = "spring.datasource") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "db1SqlSessionFactory") @Primary public 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(); } @Bean public 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") @Primary public 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") @Primary public 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 = 18800 spring.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