• 基于Mybatis及Mybatis-Plus的多数据源解决方案


    引言

    最近有项目需要支持多租户(多租户之后会单独开一篇文章说),多租户架构中需要用到多数据源,即物理隔离,需要不同租户对应不同的RMDB数据库实例,故本篇文章先行对多数据源的进行探讨。

    通常我们的工程仅存在唯一数据源以及对应的一套数据库连接池,如SpringBoot应用中如下配置:

    # 基础配置
    spring:
      datasource:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/my_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
        username: root
        password: 123456
        # Hikari 连接池配置
        hikari:
          # 最小空闲连接数量
          minimum-idle: 5
          # 空闲连接存活最大时间,默认600000(10分钟)
          idle-timeout: 180000
          # 连接池最大连接数,默认是10
          maximum-pool-size: 10
          # 此属性控制从池返回的连接的默认自动提交行为,默认值:true
          auto-commit: true
          # 连接池名称
          pool-name: MyHikariCP
          # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
          max-lifetime: 1800000
          # 数据库连接超时时间,默认30秒,即30000
          connection-timeout: 30000
          connection-test-query: SELECT 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    Mybatis生态为例,支持多数据源的方式有如下2种。

    方式1 - 使用原生Mybatis分包的方式

    此种方式需按照数据源对Mapper接口及mapper.xml进行分包,
    如下图存在2个数据源,则需要分成2个包,如ds1和ds2:
    在这里插入图片描述

    同时比较重要的是需要对Mybatis中不同包下的Mapper注入不同的DataSource,因此每个数据源都需要单独进行配置,如截图中存在2个数据源分别对应DataSourceConfig1和DataSourceConfig2两个配置类,同时需要将一个数据源设置为主数据源,避免Spring启动无法注入数据源报错。

    多数据源配置application.yml规划如下:

    spring:
      # DataSource Config
      datasource:
        ds1: # 数据源1
          # Hikari 连接池配置,具体配置属性同spring.datasource.hikari.*
          jdbc-url: jdbc:mysql://localhost:3306/multi-ds-1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.zaxxer.hikari.HikariDataSource
          # 最小空闲连接数量
          minimum-idle: 5
          # 空闲连接存活最大时间,默认600000(10分钟)
          idle-timeout: 180000
          # 连接池最大连接数,默认是10
          maximum-pool-size: 10
          # 此属性控制从池返回的连接的默认自动提交行为,默认值:true
          auto-commit: true
          # 连接池名称
          pool-name: DS1-POOL
          # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
          max-lifetime: 1800000
          # 数据库连接超时时间,默认30秒,即30000
          connection-timeout: 30000
          connection-test-query: SELECT 1
        ds2: # 数据源2
          # Hikari 连接池配置,具体配置属性同spring.datasource.hikari.*
          jdbc-url: jdbc:mysql://localhost:3306/multi-ds-2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.zaxxer.hikari.HikariDataSource
          # 最小空闲连接数量
          minimum-idle: 5
          # 空闲连接存活最大时间,默认600000(10分钟)
          idle-timeout: 180000
          # 连接池最大连接数,默认是10
          maximum-pool-size: 10
          # 此属性控制从池返回的连接的默认自动提交行为,默认值:true
          auto-commit: true
          # 连接池名称
          pool-name: DS2-POOL
          # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
          max-lifetime: 1800000
          # 数据库连接超时时间,默认30秒,即30000
          connection-timeout: 30000
          connection-test-query: SELECT 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    多数据源配置类定义如下:

    /**
     * 数据源1 - 配置
     *
     * 注:默认仅@Primary主数据源支持事务@Transactional
     *
     * @author luohq
     * @date 2022-08-06
     */
    @Configuration
    //注意此处需扫描对应数据源包下的mapper接口,且sqlSessionFactory为当前类中定义的SqlSessionFactory
    @MapperScan(basePackageClasses = {MyDataMapper1.class}, sqlSessionFactoryRef = "ds1SqlSessionFactory")
    public class DataSourceConfig1 {
    
        @Primary // 表示这个数据源是默认数据源, 这个注解必须要加,因为不加的话spring将分不清楚那个为主数据源(默认数据源)
        @Bean("ds1DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.ds1") //读取application.yml中的配置参数映射成为一个对象
        public DataSource ds1DataSource1() {
            return DataSourceBuilder.create().build();
        }
    
        @Primary
        @Bean("ds1SqlSessionFactory")
        public SqlSessionFactory ds1SqlSessionFactory(@Qualifier("ds1DataSource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            // mapper的xml形式文件位置必须要配置,不然将报错:no statement (这种错误也可能是mapper的xml中,namespace与项目的路径不一致导致)
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/ds1/*.xml"));
            return bean.getObject();
        }
    
        @Primary
        @Bean("ds1SqlSessionTemplate")
        public SqlSessionTemplate ds1SqlSessionTemplate(@Qualifier("ds1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    
    ----------------------------------------------------------------------------
    
    /**
     * 数据源2 - 配置
     * 注:默认仅@Primary主数据源支持事务@Transactional,当前非主数据源不支持事务
     * @author luohq
     * @date 2022-08-06
     */
    @Configuration
    //注意此处需扫描对应数据源包下的mapper接口,且sqlSessionFactory为当前类中定义的SqlSessionFactory
    @MapperScan(basePackageClasses = {MyDataMapper2.class}, sqlSessionFactoryRef = "ds2SqlSessionFactory")
    public class DataSourceConfig2 {
    
        @Bean("ds2DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.ds2")
        public DataSource ds2DataSource(){
            return DataSourceBuilder.create().build();
        }
    
        @Bean("ds2SqlSessionFactory")
        public SqlSessionFactory ds2SqlSessionFactory(@Qualifier("ds2DataSource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/ds2/*.xml"));
            return bean.getObject();
        }
    
        @Bean("ds2SqlSessionTemplate")
        public SqlSessionTemplate ds2SqlSessionTemplate(@Qualifier("ds2SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    在使用多数据源时,仅需将不同数据源包下的Mapper接口注入使用即可,如:

    /**
     * 

    * 我的数据 服务实现类 *

    * * @author luohq * @since 2022-08-06 */
    @Service public class MyDataServiceImpl implements IMyDataService { @Resource private MyDataMapper1 myDataMapper1; @Resource private MyDataMapper2 myDataMapper2; @Override public MyData findByIdFromDs1(Long id) { return this.myDataMapper1.selectById(id); } @Override public MyData findByIdFromDs2(Long id) { return this.myDataMapper2.selectById(id); } /** * 仅@Primary主数据源ds1支持事务,非主数据源ds2不支持事务 */ @Override @Transactional(rollbackFor = Exception.class) public Integer addBothData(MyData myData1, MyData myData2) { Integer retCount1 = this.myDataMapper1.insert(myData1); Integer retCount2 = this.myDataMapper2.insert(myData2); if (true) { throw new RuntimeException("业务异常 - 制造数据库回滚!"); } return retCount1 + retCount2; } /** * 仅@Primary主数据源支持事务 */ @Override @Transactional(rollbackFor = Exception.class) public Integer addData1(MyData myData) { Integer retCount = this.myDataMapper1.insert(myData); return retCount; } /** * 非主数据源不支持事务 */ @Override @Transactional(rollbackFor = Exception.class) public Integer addData2(MyData myData) { Integer retCount = this.myDataMapper2.insert(myData); return retCount; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    以上方式确实可以实现多数据源,但是此种方式存在如下问题:

    • @Transactional开启的事务 仅支持之前声明为 @Primary的主数据源,不支持其他 非@Primary数据源
      • 单独调用 @Primary数据源 支持事务,参见上例代码中的MyDataServiceImpl.addData1
      • 单独调用 非@Primary数据源 不支持事务,参见上例代码中的MyDataServiceImpl.addData2
      • 组合调用多数据源也仅 @Primary数据源 支持事务,如上例代码中addBothData同时调用myDataMapper1和myDataMapper2,实际测试myDataMapper1的操作支持事务,而myDataMapper2完全脱离了当前事务的管理。
      • 综上此种事务控制场景比较适合读写分离的场景(一主一从)@Primary主数据源仅作写操作,如MyDataWriteMapper.java,而其他非@Primary数据源仅作读操作,如MyDataReadMapper.java。
    • 若不同数据源对应不同的DB数据结构,又或者之前提到的读写SQL分离,则不同数据源包下定义不同的Mapper接口、Mapper.xml这种没有问题,但是对于类似多租户场景下,仅是对数据存储位置进行隔离,而不同数据源间的数据结构都是一样的,这时再在不同数据源包下维护多套相同的Mapper接口、Mapper.xml显然是不合理的。
      • 综上通过原生Mybatis分包划分多数据源的架构也不适合多租户架构

    以上示例源码参见:
    https://gitee.com/luoex/multi-datasource-demo/tree/master/mb-package-multi-ds

    方式2 - 使用Mybatis-Plus及对应的Dynamic-Datasource扩展【推荐】

    在实际开发时,我这边多数都是直接使用Mybatis-Plus作为DAO层,Mybatis-Plus作为Mybatis的增强,提供了很多开箱即用的方便特性,比如内建的CRUD操作、强大的基于Wrapper的条件构造器、分页、ID生成等等。在Mybatis-Plus生态中作者也提供了多数据源方案,即基于dynamic-datasource-spring-boot-starter的实现:

    
    <dependency>
    	<groupId>com.baomidougroupId>
    	<artifactId>mybatis-plus-boot-starterartifactId>
    	<version>3.5.2version>
    dependency>
    
    
    <dependency>
    	<groupId>com.baomidougroupId>
    	<artifactId>dynamic-datasource-spring-boot-starterartifactId>
    	<version>3.5.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    该dynamic-datasource扩展代码是开源的:
    https://github.com/baomidou/dynamic-datasource-spring-boot-starter
    https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter
    但是文档是付费的:
    https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
    我看的文档是公司小伙伴付费买的,公司内是可以传播的,但不可以在网络上传播。

    dynamic-datasource集成还是比较方便的,同时支持Druid、HikariCP等诸多连接池。
    以集成HikariCP连接池为例,application.yml配置如下:

    # dynamic-datasource多数据源配置
    spring:
      datasource:
        dynamic:
          primary: ds1 #设置默认的数据源或者数据源组,默认值即为master
          strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
          hikari: # 全局hikariCP参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
            connection-timeout: 30000
            max-pool-size: 10
            min-idle: 5
            idle-timeout: 180000
            max-lifetime: 1800000
            connection-test-query: SELECT 1
          datasource:
            ds1: # 数据源名称即对应连接池名称
              url: jdbc:mysql://localhost:3306/multi-ds-1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
              username: root
              password: 123456
              driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
              hikari: # 当前数据源HikariCP参数(继承全局、部分覆盖全局)
                max-pool-size: 20
            ds2:
              url: jdbc:mysql://localhost:3306/multi-ds-2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
              username: root
              password: 123456
              driver-class-name: com.mysql.cj.jdbc.Driver
              hikari:
                max-pool-size: 15
    
    # Mybatis-Plus相关配置
    mybatis-plus:
      global-config:
        db-config:
          id-type: assign_id
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    代码结构如下图,对比之前提到的基于原生Mybatis分包的方式,此种方式不需要对Mapper接口、Mapper.xml进行分包:
    在这里插入图片描述
    切换数据源时,可通过在Service实现类中对应方法通过@DS("具体配置中的数据源名称")指定对应的数据源:

    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.toolkit.Wrappers;
    import com.luo.demo.multi.ds.dynamic.dto.MyDataQueryDto;
    import com.luo.demo.multi.ds.dynamic.entity.MyData;
    import com.luo.demo.multi.ds.dynamic.mapper.MyDataMapper;
    import com.luo.demo.multi.ds.dynamic.service.IMyDataService;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.StringUtils;
    
    import javax.annotation.Resource;
    import java.util.Objects;
    
    /**
     * 

    * 我的数据 服务实现类 *

    * * @author luohq * @since 2022-08-07 */
    @Service public class MyDataServiceImpl implements IMyDataService { @Resource private MyDataMapper myDataMapper; @Override @DS("ds1") public MyData findByIdFromDs1(Long id) { //selectById - 支持自动拼接租户Id参数 return this.myDataMapper.selectById(id); } @Override @DS("ds1") public MyData findByQueryFromDs1(MyDataQueryDto myDataQueryDto) { //QueryWrapper - 支持自动拼接租户Id参数 return this.myDataMapper.selectOne(Wrappers.<MyData>lambdaQuery() .eq(Objects.nonNull(myDataQueryDto.getId()), MyData::getId, myDataQueryDto.getId()) .like(StringUtils.hasText(myDataQueryDto.getMyName()), MyData::getMyName, myDataQueryDto.getMyName())); } @Override public MyData findByName(String myName) { //mapper.xml自定义查询 - 支持自动拼接租户Id参数 return this.myDataMapper.selectByName(myName); } @Override @DS("ds2") public MyData findByIdFromDs2(Long id) { return this.myDataMapper.selectById(id); } /** * 单@Transactional内不支持切换数据源, * 即先使用ds1,则后续一直使用同一ds1连接, * 当前事务生效,但都会插入ds1中 */ @Override @Transactional(rollbackFor = Exception.class) public Integer addBothData(MyData myData1, MyData myData2) { Integer retCount1 = this.addData1(myData1); Integer retCount2 = this.addData2(myData2); //if (true) { // throw new RuntimeException("业务异常 - 制造数据库回滚!"); //} return retCount1 + retCount2; } /** * 支持事务 */ @Override @Transactional(rollbackFor = Exception.class) @DS("ds1") public Integer addData1(MyData myData) { //支持自动设置tenantId Integer retCount = this.myDataMapper.insert(myData); return retCount; } /** * 支持事务 */ @Override @Transactional(rollbackFor = Exception.class) @DS("ds2") public Integer addData2(MyData myData) { //支持自动设置tenantId Integer retCount = this.myDataMapper.insert(myData); return retCount; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    关于@DS注解需要注意:

    • @DS注解基于AOP实现
    • @DS推荐放在Service实现类的方法上,亦可以注解在Mapper接口上(不是Mapper方法上)
    • @DS+@Transactional支持事务,但 @Transactional方法内不支持切换数据源,参见上面示例代码中的MyDataServiceImpl.addBothData,即先使用ds1,则后续一直使用同一ds1连接,当前事务生效,但都会插入ds1中。
      • 开启了事务后,spring事务管理器会保证在事务下整个线程后续拿到的都是同一个connection。

    以上示例源码参见:
    https://gitee.com/luoex/multi-datasource-demo/tree/master/mp-dynamic-ds

    2.1 @DSTransactional

    dynamic-datasource提供自定义的 @DSTransactional 注解,

    • 支持多数据源间的本地事务
    • @DSTransactional方法内支持切换数据源,需跨服务调用切换DS,否则仅使用第一个数据源
    • @DSTransactional核心原理就是代理connection,并根据不同数据库获取到一个connection放入ConnectionFactory。 如果成功了整体提交,失败了整体回滚。

    关于使用@DSTransactional支持多数据源本地事务的示例代码如下:

    /**
     * 本地多数据源事务 - 测试服务实现类
    * * @author luohq * @date 2022-08-09 13:42 */
    @Service public class MyDataMultiDsLocalTxServiceImpl implements IMyDataMultiDsLocalTxService { @Resource private IMyDataService myDataService; /** * 此处需使用@DSTransactional,需注意不是Spring @Transactional, * 使用@DSTransactional支持切换数据源,而@Transactional方法中无法切换数据源 * 注:需跨服务调用切换DS,否则仅使用第一个数据源,即2条记录都插入到ds1中 */ @Override @DSTransactional //@DS("ds1") //如果ds1是默认数据源则不需要DS注解。 public Integer addBothData(MyData myData1, MyData myData2) { Integer retCount1 = this.myDataService.addData1(myData1); Integer retCount2 = this.myDataService.addData2(myData2); //if (true) { // throw new RuntimeException("测试多数据源异常回滚!"); //} return retCount1 + retCount2; } } ------------------------------------------------------------------------------------ /** *

    * 我的数据 服务实现类 *

    * * @author luohq * @since 2022-08-07 */
    @Service public class MyDataServiceImpl extends ServiceImpl<MyDataMapper, MyData> implements IMyDataService { @Resource private MyDataMapper myDataMapper; /** * 支持事务 - @DSTransactional区别于Spring @Transactional */ @Override @DSTransactional @DS("ds1") public Integer addData1(MyData myData) { //支持自动设置tenantId Integer retCount = this.myDataMapper.insert(myData); return retCount; } /** * 支持事务 - @DSTransactional区别于Spring @Transactional */ @DS("ds2") @Override @DSTransactional public Integer addData2(MyData myData) { //支持自动设置tenantId Integer retCount = this.myDataMapper.insert(myData); return retCount; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    示例代码中MyDataMultiDsLocalTxServiceImpl主服务,该主服务中addBothData方法跨服务调用MyDataServiceImpl中的使用@DS(“ds1)的addData1和使用@DS(“ds2”)的addData2,即主服务使用默认数据源(主服务方法亦可通过@DS(”…")指定数据源,未指定则使用默认),调用不同数据源的服务。
    关于各服务方法上事务注解及最终效果总结为下表:

    主服务
    MyDataMultiDsLocalTxServiceImpl.addBothData
    ds1服务
    @DS(“ds1”) MyDataServiceImpl.addData1
    ds2服务
    @DS(“ds1”) MyDataServiceImpl.addData1
    效果
    @DSTransactional@DSTransactional@DSTransactional调用主服务支持全局事务提交、回滚,
    单独调用ds服务各自支持事务
    @DSTransactional@Transactional@Transactional调用主服务支持全局事务提交、回滚,
    单独调用ds服务各自支持事务
    @DSTransactional调用主服务支持全局事务提交、回滚,
    单独调用ds服务不支持事务
    @DSTransactional@DSTransactional不支持全局事务,
    调用ds服务各自管理自身事务
    @TransactionalSpring @Transactional不支持切换数据源

    2.2 多数据源事务扩展

    上面提到的 @DSTransactional 支持多数据源本地事务,如何定义多数据源本地事务
    参考如下服务分布图:
    在这里插入图片描述
    其中橙黄色的ServiceA|B|C为服务实例,而蓝色Resource即为各自服务实例对应数据库存储实例,
    整个服务调用链组成了一个分布式事务,而各自Service实例自身的事务管理即为本地事务
    如下图中的绿框即标记出了各自的本地事务,其中ServiceA和ServiceC仅包含唯一的数据源,而ServiceB同时使用2个数据源,即ServiceB的需要管理的事务即为多数据源本地事务
    在这里插入图片描述
    之前提到Dynamic-Datasource中的 @DSTransactional 注解,

    • 单独使用此注解是可以解决 单体应用 或 不涉及分布式事务的单个微服务应用多数据源本地事务这种场景的

    而如上图中的完整的 分布式事务(且 存在单个服务包含多个数据源的情况) 场景,可以结合Seata:

    注:
    在引入多数据源时需谨慎,尤其是微服务场景下,可以尽早考虑将多个数据源各自拆分到不同的单个服务中,

    • 拆分后单个服务中仅包含唯一数据源,降低开发难度,便于针对单数据源进行优化,
    • 此时可直接使用Spring原生@Transactional管理单数据源本地事务(即无需引入dynamic-datasource扩展),
    • 若有分布式事务管理场景,可再引入Seata。

    2.3 多租户

    使用Mybatis-Plus及对应的Dynamic-Datasource扩展,

    • 可以共用一套Mapper接口和Mapper.xml文件,
    • 而且其除了上面提到的支持使用@DS注解切换数据源,
    • 还支持程序在运行时手动切换数据源DynamicDataSourceContextHolder.push("ds1")
      • 还支持内建的基于@DS的动态解析数据源的能力
        • @DS(“#session.tenantName”) - 从session中获取
        • @DS(“#header.tenantName”) - 从请求头中获取
        • @DS(“#user.tenantName”) - 使用SPEL从参数中获取
    • 最重要的是还支持动态添加、移除数据源

    通过上述特性描述,不难发现其和多租户架构相当契合:

    • 单个租户对应单独的数据源,可通过租户ID(如对应请求头TENANT-ID)获取对应的数据源,如
      • 租户ID直接对应数据源名称
      • 或者通过租户ID关联出对应的数据源名称
    • 不同租户用户访问系统时支持根据租户ID动态切换数据源
    • 租户支持添加或移除,同时租户对应的数据源在应用中也支持动态添或移除
      • 需考虑分布式部署中的数据源同步问题

    以上基于Mybatis-Plus及Dynamic-Datasource扩展的方案即可实现一套多租户(物理隔离)架构,多租户实现方案后续会单开一遍文章介绍,本文不做重点描述。


    以上仅介绍Mybatis-Plus及Dynamic-Datasource扩展的部分功能,感兴趣的小伙伴可以继续深入研究:

    • 如基于数据源分组的读写分离(一主多从)实现
      • master, slave_1, slave_2
      • MasterSlaveAutoRoutingPlugin
    • 自定义实现
    • 集成P6spy、Quratz、ShardingJdbc等
    • 集成Seata

    参考:

    Mybatis分包:
    springboot-整合多数据源配置(MapperScan分包、mybatis plus - dynamic-datasource-spring-boot-starter)

    Mybatis-Plus Dynamic Datasource:
    https://baomidou.com/pages/a61e1b/
    https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611

  • 相关阅读:
    【SpringMVC】详细了解SpringMVC中WEB-INF 目录资源,视图解析器和静态资源放行的使用。
    原生JavaScript实现本地存储(localStorage)和会话存储(sessionStorage)
    002从零开始入门Entity Framework Core——DbContext生存期、配置和初始化
    Matlab论文插图绘制模板第44期—二元直方图(Histogram2)
    java获取近期视频流关键帧与截图
    C++模拟揭秘刘谦魔术,领略数学的魅力
    商城小程序代客下单程序开发演示
    redis
    猿创征文|《Java》【速学】类与继承
    如何压缩jpg图片的大小
  • 原文地址:https://blog.csdn.net/luo15242208310/article/details/121330039