
一、简介
说到多数据源,一般用在以下两种场景:
多数据源的实现,从简单到复杂,有多种解决方案。
本文将以SpringBoot(2.5.X)+Mybatis+H2为例网络切换代码用不了,演示简单可靠的多数据源实现。
看完这篇文章,你会收获:
SpringBoot如何自动配置数据源? SpringBoot中Mybatis如何自动配置多数据源下的事务?如何使用它获得可靠的多数据源示例项目二、自动配置数据源
p>
SpringBoot的自动配置已经为我们做了几乎所有的工作,只需要引入相关的依赖就可以完成所有的工作
com.h2database
h2
runtime
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
当H2数据库引入依赖时,DataSourceAutoConfiguration.java会自动配置一个默认数据源:HikariDataSource,先贴上源码:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
// 1、加载数据源配置
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,
DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
// 内嵌数据库依赖条件,默认存在 HikariDataSource 所以不会生效,详见下文
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
//2、初始化带池化的数据源:Hikari、Tomcat、Dbcp2等
}
// 省略其他
}
原理如下:
1、加载数据源配置
通过@EnableConfigurationProperties(DataSourceProperties.class)加载配置信息,观察DataSourceProperties的类定义:
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean
可以得到两条信息:
配置的前缀是spring.datasource;实现了InitializingBean接口,具有初始化操作。
其实默认的嵌入式数据库连接是根据用户配置初始化的:
@Override
public void afterPropertiesSet() throws Exception {
if (this.embeddedDatabaseConnection == null) {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
}
}
通过EmbeddedDatabaseConnection.get方法遍历内置数据库枚举,找到最适合当前环境的嵌入式数据库连接。由于我们引入了H2,所以返回值也是H2数据库的枚举信息:
public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) {
return candidate;
}
}
return NONE;
}
这就是SpringBoot的约定优于配置(convention over configuration)的思想。 SpringBoot发现我们引入了H2数据库,立马准备了默认的连接信息。
2、创建数据源
默认情况下,由于SpringBoot内置了池化数据源HikariDataSource,不会加载@Import(EmbeddedDataSourceConfiguration.class),只会初始化一个HikariDataSource,因为@Conditional(EmbeddedDatabaseCondition.class)在当前不持有环境。这在源代码的注释中有解释:
/**
* {@link Condition} to detect when an embedded {@link DataSource} type can be used.
* If a pooled {@link DataSource} is available, it will always be preferred to an
* {@code EmbeddedDatabase}.
* 如果存在池化 DataSource,其优先级将高于 EmbeddedDatabase
*/
static class EmbeddedDatabaseCondition extends SpringBootCondition {
// 省略源码
}
所以默认数据源的初始化是通过:@Import({DataSourceConfiguration.Hikari.class,//Omit others}实现的。代码也比较简单:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
//创建 HikariDataSource 实例
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
protected static T createDataSource(DataSourceProperties properties, Class extends DataSource> type) {
// 在 initializeDataSourceBuilder 里面会用到默认的连接信息
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
public DataSourceBuilder> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
.url(determineUrl()).username(determineUsername()).password(determinePassword());
}
默认连接信息的使用思路是一样的:先使用用户指定的配置。如果用户不写,则使用默认值。以determineDriverClassName()为例:
public String determineDriverClassName() {
// 如果配置了 driverClassName 则返回
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(driverClassIsLoadable(), () -> "Cannot load driver class: " + this.driverClassName);
return this.driverClassName;
}
String driverClassName = null;
// 如果配置了 url 则根据 url推导出 driverClassName
if (StringUtils.hasText(this.url)) {
driverClassName = DatabaseDriver.fromJdbcUrl(this.url).getDriverClassName();
}
// 还没有的话就用数据源配置类初始化时获取的枚举信息填充
if (!StringUtils.hasText(driverClassName)) {
driverClassName = this.embeddedDatabaseConnection.getDriverClassName();
}
if (!StringUtils.hasText(driverClassName)) {
throw new DataSourceBeanCreationException("Failed to determine a suitable driver class", this,
this.embeddedDatabaseConnection);
}
return driverClassName;
}
determineUrl()、determineUsername()、determinePassword()等其他都是一样的,不再赘述。
至此,默认的HikariDataSource就自动配置好了!
看看SpringBoot中Mybatis是如何自动配置的
三、自动配置Mybatis
要在Spring中使用Mybatis,至少需要一个SqlSessionFactory和一个mapper接口,所以MyBatis-Spring-Boot-Starter为我们做了这些事情:
自动发现已有的DataSource,并将DataSource传递给SqlSessionFactoryBean,创建并注册一个SqlSessionFactory实例。使用sqlSessionFactory创建并注册SqlSessionTemplate实例,自动扫描mapper,与SqlSessionTemplate链接,注册到Spring容器中供其他bean注入
p>
结合源码加深印象:
public class MybatisAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
//1.自动发现已有的`DataSource`
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
//2.将 DataSource 传递给 SqlSessionFactoryBean 从而创建并注册一个 SqlSessionFactory 实例
factory.setDataSource(dataSource);
// 省略其他...
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
//3.利用 sqlSessionFactory 创建并注册 SqlSessionTemplate 实例
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
/**
* This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
* {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
* similar to using Spring Data JPA repositories.
*/
//4.自动扫描`mapper`,将他们与`SqlSessionTemplate` 链接起来并注册到`Spring` 容器中供其他`Bean`注入
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
// 省略其他...
}
}
一张图抵千言,其本质是层层注入:
四、从单个到多个
有了二、的三个总结的知识储备,创建多数据源的理论基础有了:两组DataSource和两组逐层注入,如图:
接下来,我们将按照自动配置单个数据源的套路来配置多个数据源,顺序如下:
首先,设计配置信息。当有单一数据源时,配置前缀为spring.datasource。为了支持多数据源,我们在后面加了一层。 yml如下:
spring:
datasource:
first:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:db1
username: sa
password:
second:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:db2
username: sa
password:
第一个数据源的配置
这里的每个@Bean 添加@Primary 使其成为默认Bean。使用@MapperScan时,指定SqlSessionTemplate,将mapper与firstSqlSessionTemplate关联起来。
提示:
最后,为数据源创建一个DataSourceTransactionManager,用于事务管理。在多数据源场景中使用事务时,@Transactional(transactionManager = “firstTransactionManager”) 用于指定事务使用哪种事务管理。
至此网络切换代码用不了,第一个数据源就配置好了,第二个数据源也配置了这些项。因为配置的bean是同一类型的,所以需要使用@Qualifier来限定加载的bean,例如:
@Bean
// 创建 SqlSessionTemplate
public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
完整的代码可以在班级代表的GitHub上查看
五、多数据源下的事务
Spring为我们提供了一个简单易用的声明式事务,让我们可以更加专注于业务开发,但要正确使用却并不容易。本文仅关注多个数据源。请点击交易补课:如何学习Spring声明式交易?
前面的小技巧中提到过,由于开启声明式事务的时候有多个事务管理器,所以需要指定使用哪个事务管理器,比如下面的例子:
// 不显式指定参数 transactionManager 则会使用设置为 Primary 的 firstTransactionManager
// 如下代码只会回滚 firstUserMapper.insert, secondUserMapper.insert(user2);会正常插入
@Transactional(rollbackFor = Throwable.class)
public void insertTwoDBWithTX(String name) {
User user = new User();
user.setName(name);
// 回滚
firstUserMapper.insert(user);
// 不回滚
secondUserMapper.insert(user);
// 主动触发回滚
int i = 1/0;
}
事务默认使用firstTransactionManager作为事务管理器,只控制FristDataSource的事务,所以当我们手动从内部抛出异常回滚事务时,firstUserMapper.insert(user);回滚,secondUserMapper.insert(user);不要回滚。
框架代码已上传,小伙伴们可以根据自己的想法设计用例验证。
六、回顾
至此,SpringBoot+Mybatis+H2的多数据源示例已经演示完毕。这应该是最基本的多数据源配置了。事实上,它很少在网上使用,除非它是一项极其简单的一次性业务。 .
因为这种方法的缺点非常明显:代码侵入性太强!需要实现的组件集就有多少个数据源,代码量呈指数级增长。
写这个案例更多的是总结和复习SpringBoot的自动配置、带注释的声明式bean、Spring声明式事务等基础知识,为后续多数据源推进铺路。
Spring官方为我们提供了一个AbstractRoutingDataSource类,通过路由DataSource来实现多个数据源的切换。这也是目前大多数轻量级多数据源实现的底层支持。
跟随类代表,下一个demo将基于AbstractRoutingDataSource+AOP多数据源实现!
参考
mybatis-spring()
mybatis-spring-boot-autoconfigure (#)
该类代表 GitHub()
请登录后发表评论
注册
社交帐号登录