乐趣区

关于java:同学你的多数据源事务失效了

一、引言

说起多数据源,个别会在如下两个场景中用到:

  • 一是业务非凡,须要连贯多个库。课代表曾做过一次新老零碎迁徙,由 SQLServer 迁徙到 MySQL,两头波及一些业务运算,罕用数据抽取工具无奈满足业务需要,只能徒手撸。
  • 二是数据库读写拆散,在数据库主从架构下,写操作落到主库,读操作交给从库,用于分担主库压力。

多数据源的实现,从简略到简单,有多种计划。

本文将以 SpringBoot(2.5.X)+Mybatis+H2 为例,演示一个 简略牢靠 的多数据源实现。

读完本文你将播种:

  1. SpringBoot是怎么主动配置数据源的
  2. SpringBoot里的 Mybatis 是如何主动配置的
  3. 多数据源下的事务如何应用
  4. 失去一个牢靠的多数据源样例工程

二、主动配置的数据源

SpringBoot的主动配置 简直 帮咱们实现了所有工作,只须要引入相干依赖即可实现所有工作

<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,先贴源码:

@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

能够失去到两个信息:

  1. 配置的前缀为spring.datasource;
  2. 实现了 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;
    }

这就是 SpringBootconvention 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,// 省略其余} 来实现的。代码也比较简单:

@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> 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 就主动配置好了!

接下来看一下 MybatisSpringBoot中是如何主动配置起来的

三、主动配置Mybatis

要想在 Spring 中应用 Mybatis,至多须要一个SqlSessionFactory 和一个 mapper 接口,所以,MyBatis-Spring-Boot-Starter 为咱们做了这些事:

  1. 主动发现已有的DataSource
  2. DataSource 传递给SqlSessionFactoryBean 从而创立并注册一个SqlSessionFactory 实例
  3. 利用sqlSessionFactory 创立并注册 SqlSessionTemplate 实例
  4. 主动扫描 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 如下:

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数据源的配置

/**
 * @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,将mapperfirstSqlSessionTemplate分割起来。

小贴士:

最初还为该数据源创立了一个 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,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 的主动配置,注解式申明 BeanSpring 申明式事务等基础知识,为前面的多数据源进阶做铺垫。

Spring 官网为咱们提供了一个 AbstractRoutingDataSource 类,通过对 DataSource 进行路由,实现多数据源的切换。这也是目前,大多数轻量级多数据源实现的底层撑持。

关注课代表,下一篇演示基于 AbstractRoutingDataSource+AOP 的多数据源实现!

七、参考

mybatis-spring

mybatis-spring-boot-autoconfigure

课代表的 GitHub

退出移动版