同學,你的多數據源事務失效了
一、引言
説起多數據源,一般會在如下兩個場景中用到:
-
一是業務特殊,需要連接多個庫。課代表曾做過一次新老系統遷移,由
SQLServer
遷移到MySQL
,中間涉及一些業務運算,常用數據抽取工具無法滿足業務需求,只能徒手擼。 -
二是數據庫讀寫分離,在數據庫主從架構下,寫操作落到主庫,讀操作交給從庫,用於分擔主庫壓力。
多數據源的實現,從簡單到複雜,有多種方案。
本文將以SpringBoot(2.5.X)+Mybatis+H2
為例,演示一個簡單可靠的多數據源實現。
讀完本文你將收穫:
SpringBoot
是怎麼自動配置數據源的SpringBoot
裏的Mybatis
是如何自動配置的- 多數據源下的事務如何使用
- 得到一個可靠的多數據源樣例工程
二、自動配置的數據源
SpringBoot
的自動配置~~幾乎~~幫我們完成了所有工作,只需要引入相關依賴即可完成所有工作
java
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
當依賴中引入了H2
數據庫後,DataSourceAutoConfiguration.java
會自動配置一個默認數據源:HikariDataSource
,先貼源碼:
```java @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
的類定義:
java
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean
可以得到到兩個信息:
- 配置的前綴為
spring.datasource
; - 實現了
InitializingBean
接口,有初始化操作。
其實是根據用户配置初始化了一下默認的內嵌數據庫連接:
java
@Override
public void afterPropertiesSet() throws Exception {
if (this.embeddedDatabaseConnection == null) {
this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader);
}
}
通過EmbeddedDatabaseConnection.get
方法遍歷內置的數據庫枚舉,找到最適合當前環境的內嵌數據庫連接,由於我們引入了H2
,所以返回值也是H2
數據庫的枚舉信息:
java
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)
在當前環境下不成立。這點在源碼裏的註釋已經解釋了:
```java /** * {@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,//省略其他}
來實現的。代碼也比較簡單:
```java @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; }
} ```
java
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
// 在 initializeDataSourceBuilder 裏面會用到默認的連接信息
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
java
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
.url(determineUrl()).username(determineUsername()).password(determinePassword());
}
默認連接信息的使用都是同樣的思想:優先使用用户指定的配置,如果用户沒寫,那就用默認的,以determineDriverClassName()
為例:
java
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
就自動配置好了!
接下來看一下Mybatis
在SpringBoot
中是如何自動配置起來的
三、自動配置Mybatis
要想在Spring
中使用Mybatis
,至少需要一個SqlSessionFactory
和一個mapper
接口,所以,MyBatis-Spring-Boot-Starter
為我們做了這些事:
- 自動發現已有的
DataSource
- 將
DataSource
傳遞給SqlSessionFactoryBean
從而創建並註冊一個SqlSessionFactory
實例 - 利用
sqlSessionFactory
創建並註冊SqlSessionTemplate
實例 - 自動掃描
mapper
,將他們與SqlSessionTemplate
鏈接起來並註冊到Spring
容器中供其他Bean
注入
結合源碼加深印象:
``
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
如下:
yaml
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:
first
數據源的配置
```java /* * @description: * @author:Java課代表 * @createTime:2021/11/3 23:13 / @Configuration //配置 mapper 的掃描位置,指定相應的 sqlSessionTemplate @MapperScan(basePackages = "top.javahelper.multidatasources.mapper.first", sqlSessionTemplateRef = "firstSqlSessionTemplate") public class FirstDataSourceConfig {
@Bean
@Primary
// 讀取配置,創建數據源
@ConfigurationProperties(prefix = "spring.datasource.first")
public DataSource firstDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
// 創建 SqlSessionFactory
public SqlSessionFactory firstSqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 設置 xml 的掃描路徑
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/first/*.xml"));
bean.setTypeAliasesPackage("top.javahelper.multidatasources.entity");
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(config);
return bean.getObject();
}
@Bean
@Primary
// 創建 SqlSessionTemplate
public SqlSessionTemplate firstSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
@Primary
// 創建 DataSourceTransactionManager 用於事務管理
public DataSourceTransactionManager firstTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
``
這裏每個
@Bean都添加了
@Primary使其成為默認
Bean,
@MapperScan使用的時候指定
SqlSessionTemplate,將
mapper與
firstSqlSessionTemplate`聯繫起來。
小貼士:
最後還為該數據源創建了一個
DataSourceTransactionManager
,用於事務管理,在多數據源場景下使用事務時通過@Transactional(transactionManager = "firstTransactionManager")
用來指定該事務使用哪個事務管理。
至此,第一個數據源就配置好了,第二個數據源也是配置這些項目,因為配置的Bean類型相同,所以需要使用@Qualifier
來限定裝載的Bean
,例如:
java
@Bean
// 創建 SqlSessionTemplate
public SqlSessionTemplate secondSqlSessionTemplate(@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
完整代碼可查看課代表的GitHub
五、多數據源下的事務
Spring
為我們提供了簡單易用的聲明式事務,使我們可以更專注於業務開發,但是想要用對用好卻並不容易,本文只聚焦多數據源,關於事務補課請戳:Spring 聲明式事務應該怎麼學?
前文的小貼士裏已經提到了開啟聲明式事務時由於有多個事務管理器存在,需要顯示指定使用哪個事務管理器,比如下面的例子:
```java // 不顯式指定參數 transactionManager 則會使用設置為 Primary 的 firstTransactionManager // 如下代碼只會回滾 firstUserMapper.insert, secondUserMapper.insert(user2);會正常插入 @Transactional(rollbackFor = Throwable.class,transactionManager = "firstTransactionManager") 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
進行路由,實現多數據源的切換。這也是目前,大多數輕量級多數據源實現的底層支撐。
關注課代表,下一篇演示基於AbstractRoutingDataSource+AOP
的多數據源實現!