談一談Spring-Mybatis在多數據源配置上的坑

  • 蘇格團隊
  • 做者:JayceKon
  • 交流QQ羣:855833773
  • 歡迎加入咱們的團隊,微信聯繫方式:foreverpx_cjl

概述

先聊一聊業務背景,隨着系統服務的不斷開發,咱們的系統會充斥着各類個樣的業務.這種時候,咱們應該要開始考慮一下如何將系統的粒度細化.舉個常見的例子: 電商系統能夠拆分爲 商品模塊,訂單模塊,地址模塊等等.這些模塊均可以獨立抽取出來,造成一個單獨的服務.這就會涉及到各個模塊之間的通訊問題,一些簡單的服務,咱們能夠經過 rpc 接口 直接進行通訊,可是有些服務卻不適用這種模式.本文主要講一下在多數據源路上遇到的一些坑.java

多數據源

項目結構

源碼地址: github.com/jaycekon/Sp…mysql

目錄結構

配置文件: DataSourceConfiggit

@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 讀寫分離》github

數據庫

test_1:spring

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:sql

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;
複製代碼

一、數據庫連接異常

此數據庫連接異常,指的是在 切換數據源 時,數據庫連接異常數據庫

啓動咱們的服務:apache

說明咱們的服務配置是沒有什麼問題的,那麼所謂的數據庫連接異常又是什麼回事呢?bash

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
複製代碼

咱們建立了一個測試類,來檢測兩個數據源處理狀況

從結果來看:

一、schoolService 成功了 (db:test_1)

二、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 中的數據源連接異常

異常分析

一、數據源連接的是 test_1 說明沒有成功切換數據源

二、觀察切面方法,監聽的是 dataSource

@Before("@annotation(com.jaycekon.mybatis.multi.config.DataSource)")
複製代碼

三、@DataSource

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface DataSource 
複製代碼

經過上述註解能夠發現,咱們註解對象爲 TYPE(類),而在 AspectJ 中的註解監聽,只支持方法註解監聽,並不能監聽類的註解.所以,在上述咱們經過註解整個類的方式,並不能作到數據源動態切換:

@Service
@DataSource("db2")
public class UserService

@Service
@DataSource("db1")
public class SchoolService
複製代碼

解決辦法

一、修改 DataSource 爲方法註解,對每一個須要切換數據源的方法進行監聽.該方法 比較.

二、經過@Pointcut("execution(* com.jaycekon.demo.mapper.*.*(..))") 經過Pointcut 的形式,能夠監聽到某個包下面的全部類,全部方法.這個方法還行,可是每次若是建立了新的類,有可能須要修改配置.

三、目前採用的方式爲,將不一樣數據源的mapper,type-aliases,config 分開 配置方式可參考: 傳送門

修改後目錄(配置文件只需保留兩項便可):

二、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) 卻出了問題.

異常分析

一、Mapper 掃描沒有找到對應的 XML 文件

二、多數據源存在多個 SqlSessionFactory ,須要將 Mapper文件綁定到對應的 SqlSessionFactory

三、解決辦法,在掃描 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"));
        ...
    }
複製代碼

可是他並無起任何做用,這是爲何呢?

異常分析

一、別名掃描沒有起做用

二、到Github 查找相關內容,會發現有相同的經歷: 傳送門

解決辦法

一、不使用別名(不是個好辦法)

二、在mybatis/spring-boot-starter 這個項目中,提出了一個官方的 Demo

咱們截取中間比較關鍵的一部分代碼:

SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
複製代碼

咱們採用方法2 嘗試一下,看看能不能解決問題:

關於 VFS 的一些解釋:

虛擬文件系統(VFS),用來讀取服務器裏的資源
複製代碼

我的理解爲,新建立的 SqlSessionFactory 沒有可以加載配置文件,致使除 @Primary 外的全部 SqlSessionFactory 都沒辦法加載相關配置文件.

三、Config 異常

一路配置下來,單元測試跑通了,服務啓動也成功了,接下來就是一頓騷操做,各類功能開發~ 在開發完成後,進入測試階段.一看數據返回,坑爹啊~~

怎麼返回了個空數據?

異常分析

一、數據有返回,服務沒有問題

二、schoolName 對應 數據庫 school_name,中間轉換須要使用駝峯命名轉換

駝峯命名轉換 mybatis.configuration.map-underscore-to-camel-case 出問題了.

解決辦法

一、添加配置 mybatis.configuration.map-underscore-to-camel-case=true

二、建立 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);
        ...
    }
複製代碼

三、@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

複製代碼

四、事務異常

因爲咱們在多數據源中,採用了多 sqlSessionFactory 方式,所以在事務管理這塊,會出現事務管理異常相關問題,有興趣的童鞋能夠參考:www.atomikos.com/Main/WebHom… ,推薦一個整合的 Demo

總結

Mybatis 多數據源配置主要分兩種,一種動態配置數據源 & 一種配置多 sqlsessionFactory,本文的一些坑,主要基於 多 sqlSessionFactory. 上述的全部問題,都是在開發過程當中所遇到,可能各位或多或少有遇到過,但願能給各位相關幫助.

如對我的看法有所異議,歡迎指正.

Demo地址: github.com/jaycekon/Sp…

相關文章
相關標籤/搜索