背景信息

技术框架背景信息

  • SpringBoot 2.5
  • MyBatis-Plus 3.4.3.1
  • dynamic-datasource-spring-boot-starter 3.4.1
  • MySQL 5.7
  • 连接池:HikariCP(SpringBoot集成版本)

业务背景

笔者所维护的一个项目使用了SpringBoot+MyBatisPlus框架,单库框架。由于项目的数据库连接参数放在了Nacos的配置中心,并通过@ConfigurationProperties注解构建了一个配置Bean的方式导入配置,所以数据库连接没有使用SpringBoot的yaml进行配置,而是使用了JavaConfig的配置方式。
由于业务需要,当前站点需要连接一个新的库。这样就涉及到了在MyBatis-Plus框架下的多数据源连接。
在各大技术论坛上,MyBatis-Plus下的多数据源方案主要有以下2种

  • 自定义AOP,根据请求的Mapper属性自动切换数据源(技术文献
  • 使用dynamic-datasource框架(官方文档推荐)

经过谨慎选择,笔者决定使用dynamic-datasource的方案。

TIPS

在这里要澄清一下,并不是说使用了Nacos的配置中心后,就不能使用autoconfiguration功能了。事实上将yaml的配置从application.yaml迁移至配置中心后,同样可以参照原有配置格式进行配置。写这篇文章只是为了跟大家探究一下dynamic-datasource的工作本质和通过autoconfiguration进行的工作机制

dynamic-datasource

这是一款在DataSource层面的数据库动态切换框架。基础信息如下

配置及原理探究

名词解释

下文中所涉及到的一些名词统一在此做解释。

名词 解释
demo.* 【包名】代表着项目内部包路径,统一使用了demo.代替真实包名
DatabaseConnectionProperties 【配置类】通过@ConfigurationProperties从配置中获取的实体配置Bean,里面存储了数据库连接的账户信息
DatabaseConstants 【常量类】通过常量类的方式固定了数据库相关的常量信息
DB_NAME_1 【常量值】DatabaseConstants下存储的数据库1的数据库名
DB_NAME_2 【常量值】DatabaseConstants下存储的数据库2的数据库名

ORM框架配置原理解析

在Spring框架下,通过ORM框架(以Mybatis举例)访问数据库,通常需要构建如下几个Bean

  • JDBC连接
  • DataSource
  • ORM框架(SqlSessionFactory)
  • TransactionManager

而如上各个Bean的构建顺序及层级关系如下

扫描
JDBC连接
DataSource
ORM框架-SqlSessionFactory
MapperBean
TransactionManager
Interceptor插件

dynamic-datasource的工作层级分析

我们从dynamic-datasource框架的官方文档中,可以追踪到配置类DynamicDataSourceProperties,同时,查找其调用,可追踪到自动配置类DynamicDataSourceAutoConfiguration,如下图:
追踪DynamicDataSourceProperties至DynamicDataSourceAutoConfiguration
根据DynamicDataSourceAutoConfiguration,我们可得知,dynamic-datasource构建了如下主要Bean

Bean名称 用途
dataSource 实现了DataSource接口,实际是一个自定义的动态数据源实现DynamicRoutingDataSource,具有@ConditionalOnMissingBean注解性质
dynamicDatasourceAnnotationAdvisor 实现了Advisor接口,实质上是针对@DS注解的一个切面,用于切换数据源
dynamicTransactionAdvisor 实现了Advisor接口,事实上这个AOP实例实现了通过@DSTransactional注解在多数据源下的提交和回滚。具体分析可以看这一篇文章《DSTransactional实现源码分析》

其它Bean用于辅助上述三个Bean的构造,并不是重点。
所以,至此我们已经能够很清晰地了解到,dynamic-datasource实际是工作在了DataSource层,通过自定义的DataSource实现和AOP实现的配合,完成数据源切换。

Mybatis-Plus+dynamic-datasource框架下的配置构建

框架构建图

根据上一小节的分析,在Mybatis-Plus+dynamic-datasource组合下,数据访问框架的实际配置流程如下:

扫描
DynamicRoutingDataSource
DynamicDataSourceProperties
Datasource1Property
Datasource2Property
MyBatisPlus-SqlSessionFactory
MapperBean
TransactionManager-1
TransactionManager-2
Interceptor插件

JavaConfig配置构建说明

这里我们虽然不使用典型AutoConfiguration配置方式,即不在配置文件中写配置,但是我们依旧会利用AutoConfiguration框架,来辅助我们避免繁琐配置。具体方法是:

  1. 手动构建DynamicDataSourceProperties的Bean,并为其加上@Primary注解,使得其可以被DynamicDataSourceAutoConfiguration使用,完成部分Bean的操作
  2. 参照DynamicDataSourceAutoConfiguration中DataSource的构建方式,手动构建DynamicRoutingDataSource。(注意:DynamicDataSourceAutoConfiguration中DataSource有@ConditionalOnMissingBean注解性质,所以手动构建后,其内部的dataSource就不会再构建了)
  3. 通过DynamicRoutingDataSource内方法获取到的各个库的独立dataSource,构建TransactionManager

JavaConfig配置代码拆解

下面会根据上方配置层级图的方式,从底层开始逐层展示各个Bean的构建代码

包路径及引用

所有的包引用都在这

package demo.config;

import cn.hutool.db.dialect.DialectFactory;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.hikari.HikariCpConfig;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import demo.config.properties.DatabaseConnectionProperties;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

import static demo.constants.DatabaseConstants.*;
类及类变量
/**
 * MySqlConfig
 * 用于配置MySQL连接
 *
 * @author John Chen
 * @since 2021/7/22
 */
@Configuration
@MapperScan("demo.*.mapper")
public class MySqlConfig {
    /**
     * 数据库连接参数配置
     */
    private final DatabaseConnectionProperties properties;

    public MySqlConfig(DatabaseConnectionProperties properties) {
        this.properties = properties;
    }

}
DynamicDataSourceProperties

这里构建时必须加上@Primary,否则Spring会报有多个Bean实例的错误

    @Primary
    @Bean
    public DynamicDataSourceProperties dynamicDataSourceProperties() {
        DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
        properties.setPrimary(DB_NAME_1);
        properties.setStrict(true);
        properties.setHealth(true);
        properties.getDatasource().put(DB_NAME_2, db2DataSourceProperty());
        properties.getDatasource().put(DB_NAME_1, db1DataSourceProperty());
        return properties;
    }

    private DataSourceProperty db2DataSourceProperty() {
        DataSourceProperty property = new DataSourceProperty();
        DatabaseConnectionProperties.MySQLConfigEntity configEntity = properties.getDb2();
        property.setUrl(configEntity.getUrl());
        property.setUsername(configEntity.getUsername());
        property.setPassword(configEntity.getPassword());
        property.setDriverClassName(DialectFactory.DRIVER_MYSQL_V6);
        //连接池名称配置,默认为HikariDataSource+数据库名称+Pool
        property.setPoolName("HikariDataSourcePool_" + DB_NAME_2);
        HikariCpConfig hikariConfig = new HikariCpConfig();
        //region 固定连接池配置
        hikariConfig.setMinIdle(1);
        hikariConfig.setMaxPoolSize(15);
        hikariConfig.setIdleTimeout(30000L);
        hikariConfig.setMaxLifetime(1800000L);
        hikariConfig.setConnectionTimeout(30000L);
        hikariConfig.setConnectionTestQuery("select 1");
        hikariConfig.setConnectionInitSql("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
        property.setHikari(hikariConfig);
        return property;
    }

    private DataSourceProperty db1DataSourceProperty() {
        DataSourceProperty property = new DataSourceProperty();
        DatabaseConnectionProperties.MySQLConfigEntity configEntity = properties.getDb1();
        property.setUrl(configEntity.getUrl());
        property.setUsername(configEntity.getUsername());
        property.setPassword(configEntity.getPassword());
        property.setDriverClassName(DialectFactory.DRIVER_MYSQL_V6);
        //连接池名称配置,默认为HikariDataSource+数据库名称+Pool
        property.setPoolName("HikariDataSourcePool_" + DB_NAME_1);
        HikariCpConfig hikariConfig = new HikariCpConfig();
        //region 固定连接池配置
        hikariConfig.setMinIdle(1);
        hikariConfig.setMaxPoolSize(15);
        hikariConfig.setIdleTimeout(30000L);
        hikariConfig.setMaxLifetime(1800000L);
        hikariConfig.setConnectionTimeout(30000L);
        hikariConfig.setConnectionTestQuery("select 1");
        hikariConfig.setConnectionInitSql("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
        property.setHikari(hikariConfig);
        return property;
    }
DynamicRoutingDataSource

这个DataSource实例会代替原配置类中的bean构建

    /**
     * 这里通过dynamic-datasource框架的自定义动态数据源构建了一个{@link DataSource}的Bean
     * 注意,如果要在多数据源下试用统一提交和回滚事务,请试用注解{@link com.baomidou.dynamic.datasource.annotation.DSTransactional}
     * 详情见文章《DSTransactional实现源码分析》
     * <p>
     * https://www.yinxiang.com/everhub/note/ac0175c8-35f5-4d66-8cd3-c662d7a16441
     *
     * @param properties 多数据源对应配置
     * @return 返回DataSource
     */
    @Bean
    public DynamicRoutingDataSource dataSource(DynamicDataSourceProperties properties) {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        dataSource.setPrimary(properties.getPrimary());
        dataSource.setStrict(properties.getStrict());
        dataSource.setStrategy(properties.getStrategy());
        dataSource.setP6spy(properties.getP6spy());
        dataSource.setSeata(properties.getSeata());
        return dataSource;
    }


MyBatis-Plus构建(包含插件构建)

其实最开始我自己也不清楚加入了dynamic-datasource后,MyBatis-Plus应该如何构建。事实上由于2套框架工作在不同层,他们的配置完全不会互相影响。所以这里依旧是MyBatis-Plus的传统的JavaConfig构建方式。

    /**
     * 构建SqlSessionFactory
     * <p>
     * 注意,这里构建后会影响自动配置类{@link com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory(DataSource)}
     * 的构建。后期如果增加插件,请务参照对照方法在内部增加注入配置
     */
    @Bean
    public SqlSessionFactory SqlSessionFactory(DataSource dataSource,GlobalConfig globalConfig, MybatisPlusInterceptor paginationInterceptor) throws Exception {
        final MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/**/**Mapper.xml")
        );
        sqlSessionFactoryBean.setTypeAliasesPackage("demo.mapper.*");
        sqlSessionFactoryBean.setTypeEnumsPackage("demo.*.enumutil");
        sqlSessionFactoryBean.setGlobalConfig(globalConfig);
        sqlSessionFactoryBean.setConfiguration(defaultMybatisConfiguration());

        Interceptor[] plugins = new Interceptor[]{paginationInterceptor};
        sqlSessionFactoryBean.setPlugins(plugins);
        return sqlSessionFactoryBean.getObject();
    }

   /**
     * 默认的MybatisPlus配置
     *
     * @return MybatisPlus配置
     */
    private MybatisConfiguration defaultMybatisConfiguration() {
        MybatisConfiguration configuration = new MybatisConfiguration();
        //打开自动驼峰命名转换
        configuration.setMapUnderscoreToCamelCase(true);
        //打开二级缓存
        configuration.setCacheEnabled(true);
        //打开延迟加载开关
        configuration.setLazyLoadingEnabled(true);
        //将积极加载改为消极按需加载
        configuration.setAggressiveLazyLoading(false);
        //是否允许单一语句返回多结果集(需要兼容驱动)。[默认值]
        configuration.setMultipleResultSetsEnabled(true);
        //使用列标签代替列名。不同的驱动在这方面会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。[默认值]
        configuration.setUseColumnLabel(true);
        //使用MybatisPlus的枚举处理器,用于完成枚举处理
        configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
        return configuration;
    }


    /**
     * 创建全局配置
     *
     * @return 全局配置
     */
    @Bean
    public GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        // 默认为自增
        dbConfig.setIdType(IdType.AUTO);
        globalConfig.setDbConfig(dbConfig);
        return globalConfig;
    }

    /**
     * 分页插件,自动识别数据库类型
     */
    @Bean
    public MybatisPlusInterceptor paginationInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

TransactionManager

这里的TransactionManager要注意一下,由于这里的事务管理器是针对具体数据源进行构建的,所以后期在使用@Transactional注解时要注意:

  1. 注解中要标明对应的事务管理器实现
  2. 被注解的方法内,不要出现跨数据源调用。否则数据源切换会失效。详见文档说明
  3. 如果存在跨数据源调用的情况,请使用@DSTransactional
    @Bean
    @Primary
    public DataSourceTransactionManager db1TransactionManager(DynamicRoutingDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource.getDataSource(DB_NAME_1));
    }

    @Bean
    public DataSourceTransactionManager db2TransactionManager( DynamicRoutingDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource.getDataSource(DB_NAME_2));
    }

其它ORM框架下dynamic-datasource的使用

事实上,到这里我们已经发现,虽然MyBatis-Plus官方文档中推荐使用了dynamic-datasource这个组件做多数据源切换。但这个组件实际并没有和MyBatis-Plus有任何耦合。相反,该组件由于工作在DataSource层,可以工作在任何使用DataSource作为底层连接源的ORM框架下。具体配置可参照具体ORM框架配置说明和下方配置关系图

DynamicRoutingDataSource
DynamicDataSourceProperties
DatasourceProperty1
DatasourceProperty2
DatasourceProperty...
DatasourcePropertyN
ORM框架
TransactionManager-...

使用注意事项

  • 本文仅做了dynamic-datasource的构建分析,具体这个框架的使用请参考其技术文档和源码
  • 根据一些技术文献的描述来看,@DSTransactional是一个不完全的事务管理器。使用时务必先了解其特性
  • JavaConfig完成了更加复杂的场景下的实例配置。而AutoConfiguration让我们能够更加轻松地完成项目配置。二者本质上没有优劣之分。但我个人更喜欢JavaConfig,因为通过JavaConfig配置,你能够更加深入地了解构建对象的基础工作原理,对于技术也是个不错的提升机会。
  • 如发现本文不成熟之处,欢迎留言或私信提出你的宝贵建议~
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐