谈一谈Spring-Mybatis在多数据源配置上的坑

概述

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

多数据源

13465705-ef4b5035cd3aefd8.png

项目结构

13465705-8d6057916ce869dc.png

配置文件: 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();

    }

数据库

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、数据库链接异常

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

启动我们的服务:

13465705-853df8d20fba78a0.png

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

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

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

13465705-1849f4a9fec3fc51.png

从结果来看:

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分开

修改后目录(配置文件只需保留两项即可):


13465705-cbed5b10556c6a85.png

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)的时候,问题就来了:

13465705-5d2e2abf0dc35b5a.png

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

13465705-5a55f03dfaca43d5.png
13465705-7cf3dc397e1b9fa9.png

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

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、别名扫描没有起作用

解决办法

1、不使用别名(不是个好办法)

13465705-be15e41189da6355.png

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

SqlSessionFactoryBean factory = new SqlSessionFactoryBean();

    factory.setDataSource(dataSource);

    factory.setVfs(SpringBootVFS.class);

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

13465705-846f27e55f74f0f9.png

关于 VFS 的一些解释:

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

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

3、Config 异常

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

13465705-93fc3af67a08502f.png

怎么返回了个空数据?

异常分析

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

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

13465705-b4c7a08a62ba9360.png

驼峰命名转换 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

总结

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

欢迎工作一到五年的Java工程师朋友们加入Java进阶架构学习交流:952124565,群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值