- 苏格团队
- 做者:JayceKon
- 交流QQ群:855833773
- 欢迎加入咱们的团队,微信联系方式:foreverpx_cjl
先聊一聊业务背景,随着系统服务的不断开发,咱们的系统会充斥着各类个样的业务.这种时候,咱们应该要开始考虑一下如何将系统的粒度细化.举个常见的例子: 电商系统能够拆分为 商品模块,订单模块,地址模块等等.这些模块均可以独立抽取出来,造成一个单独的服务.这就会涉及到各个模块之间的通讯问题,一些简单的服务,咱们能够经过 rpc
接口 直接进行通讯,可是有些服务却不适用这种模式.本文主要讲一下在多数据源
路上遇到的一些坑.java
源码地址: github.com/jaycekon/Sp…mysql
配置文件: DataSourceConfig
git
@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
分开 配置方式可参考: 传送门
修改后目录(配置文件只需保留两项便可):
在咱们修改新的配置文件后,能够参考下面代码(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
若是你以为这个坑到这里就结束了,你就过小看我了~
正常来讲,咱们单元测试 & 服务都没有问题,讲道理是可以正常进行接下来的开发了.可是,咱们若是使用的是 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
都没办法加载相关配置文件.
一路配置下来,单元测试跑通了,服务启动也成功了,接下来就是一顿骚操做,各类功能开发~ 在开发完成后,进入测试阶段.一看数据返回,坑爹啊~~
怎么返回了个空数据?
一、数据有返回,服务没有问题
二、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…