概述

先聊一聊业务背景,进行系统服务的不断开发,我们的系统会充斥着各种个样的业务。这种时候,我们应该开始考虑一下如何将系统的粒度细化。举个常见的例子:电商系统可以分解为商品模块,订单模块,地址模块等等。这些模块都可以独立摘出来,形成一个单独的服务。这就会涉及到各个模块之间的通信问题,一些简单的服务,我们可以通过rpc接口直接进行通信,但是某些服务却不适用这种模式。多数据源此处主要讲一下在路上遇到的一些坑。

多数据源

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

项目结构

原始网址:github.com/jaycekon/Sp ...

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

配置文件: 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读写分离》

数据库

测试_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;

测试_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,数据库链接异常

此数据库链接异常,指的是在切换数据源时,数据库链接异常

启动我们的服务:

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

说明我们的服务配置是没有什么问题的,那么所谓的数据库链接异常又是什么回事呢?

测试:

    @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

我们创建了一个测试类,来检测两个数据源处理情况

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

从结果来看:

如图1所示,schoolService成功了(分贝:test_1)

2,UserService失败了(分贝:test_2)

错误信息:

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.*.*(..))")PointPoint的形式,可以监听到某个包下面的所有类,所有方法。这个方法还行,但是每次如果创建了新的类,有可能需要修改配置。

3,目前采用的方式为,将不同数据源的mapper,type-aliases,config分开配置方式可参考:传送门

修改后目录(配置文件只需保留一部分即可):

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

2,映射器映射异常

在我们修改新的配置文件后,可以参考以下代码(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)的时候,问题就来了:

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

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

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

如果你觉得这个坑到这里就结束了,你就太小看我了〜

2.1 TypeAliases映射

正常来说,我们单元测试和服务都没有问题,讲道理是能够正常进行后续的开发了。但是,我们如果使用的是Spring-Boot进行开发,那我们在发布前就还需要做一个操作打包Jar包,而用命令行启动服务:

java -jar target/spring-boot-mybatis-multi.jar

然后,然后就会出现预定问题:

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,不使用别名(不是个好办法)

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

2,在mybatis/spring-boot-starter这个项目中,提出了一个官方的Demo

我们截取中间比较关键的一部分代码:

    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);

我们采用方法2尝试一下,看看能不能解决问题:

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

关于VFS的一些解释:

虚拟文件系统(VFS),用来读取服务器里的资源

个人理解为,创建³³新的SqlSessionFactory没有能够加载配置文件,除导致@Primary外的所有SqlSessionFactory都没办法加载相关配置文件。

3,配置异常

一路配置下来,单元测试跑通了,服务启动也成功了,接下来就是一顿骚操作,各种功能开发〜在开发完成后,进入测试阶段。一看数据返回,坑爹啊~~

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

怎么返回了个空数据?

异常分析

1,数据有返回,服务没有问题

2,对应schoolName数据库school_name,中间转换需要使用驼峰命名转换

一定要了解!Spring-Mybatis在多数据源配置上的坑

 

驼峰命名转换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")此处配置类使用的是多实例作用域,主要是为了解决单例模式会影响到数据源的链接。

数据库连接超时

当你屁颠屁颠的把项目发布到服务器,接口调试都没有问题。过一晚突然发现,服务挂了,会发生什么?

{
    "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,将会造成上面所以配置数据源时需要配置相应的连接池参数,定时去检查连接的有效性,定时清理无效的连接。引用

解决方案-完善相关配置:

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...,推荐一个整合的演示版

总结

Mybatis多数据源配置主要分为两种,一种动态配置数据源&一种配置多sqlsessionFactory,此处的一些坑,主要基于多sqlSessionFactory。上述的所有问题,都是在开发过程中所遇到的,可能各位或多或少有遇到过,希望能给各位相关帮助。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐