SpringBoot多数据源处理方案、多租户实战

业务场景:

  • 针对某些服务端端,一般都是单体部署,并没有严格按照微服务原则进行数据隔离,如何让这个单体服务去管理多个数据库?
  • 如果使用ShardingSphere,那么在这个场景下,会不会太重了,他是做分库分表的,大部分业务场景只需要做简单的查询就可以了
  • 一般像常见的电商后台项目,大多使用的MyBatis-plus框架访问数据库。对于MyBatisMyBatis-plus框架,这里不多做介绍了。跨多个数据库管理的后台数据。这里分享三种常用的多数据源管理方案:


一、使用Spring提供的AbstractRoutingDataSource

这种方式的核心是使用Spring提供的AbstractRoutingDataSource抽象类,注入多个数据源。

1、配置多数据源

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource1:
      url: jdbc:mysql://127.0.0.1:3306/datasource1?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root123456
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver
    datasource2:
      url: jdbc:mysql://127.0.0.1:3306/datasource2?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: root123456
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver

2、配置类

/***
 * @Author 摸鱼码长
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }


    @Bean
    public DataSourceTransactionManager transactionManager1(DynamicDataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }

    @Bean
    public DataSourceTransactionManager transactionManager2(DynamicDataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

3、为targetDataSources初始化所有数据源

/***
 * @Author 摸鱼码长
 */
@Component
@Primary   // 将该Bean设置为主要注入Bean
public class DynamicDataSource extends AbstractRoutingDataSource {


    // 当前使用的数据源标识
    public static ThreadLocal<String> name=new ThreadLocal<>();

    // 库1
    @Autowired
    DataSource dataSource1;
    // 库2
    @Autowired
    DataSource dataSource2;


    // 返回当前数据源标识
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();

    }


    @Override
    public void afterPropertiesSet() {

        // 为targetDataSources初始化所有数据源
        Map<Object, Object> targetDataSources=new HashMap<>();
        targetDataSources.put("s1",dataSource1);
        targetDataSources.put("s2",dataSource2);

        super.setTargetDataSources(targetDataSources);

        // 为defaultTargetDataSource 设置默认的数据源
        super.setDefaultTargetDataSource(dataSource1);

        super.afterPropertiesSet();
    }
}

思考

如果要切换数据源,我要在每个方法都写上这么一个DynamicDataSource.name.set(name);那么对业务的侵入是不是太重了?更优雅的解决方案,使用切片!

4、封装成切面处理

@Component
@Aspect
public class DynamicDataSourceAspect implements Ordered {
    // 在每个访问数据库的方法执行前执行。(扫描这个包下有没有WR注解这个方法)
    @Before("within(com.tuling.dynamic.datasource.service.impl.*) && @annotation(wr)")
    public void before(JoinPoint point, WR wr){
        // 在开始之前找到wr这个value,然后到DynamicDataSource进行切换
        String name = wr.value();
        DynamicDataSource.name.set(name);
        System.out.println(name);
    }
    @Override
    public int getOrder() {
        return 0;
    }
}
/***
 * @Author 摸鱼码长
 * 
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
    String value() default "s1";
}

二、使用MyBatis注册多个SqlSessionFactory

mybatis其实他是只支持单数据源的?那么他的注入方式是什么呢?其实是通过他后台注入的SqlSessionFactory对象,他底层会引入spring容器中的DataSource组件来构建一个SqlSessionFactory还会构建一个DataSourceTransactionManager这些对象。

那么我们想让mybatis管理多个数据源怎么办?

只能我们自己去重新注册这些组件,让mybatis针对每一套数据源重新组装一个mybatis出来,就需要将MyBatis底层的DataSource、SqlSessionFactory、DataSourceTransactionManager这些核心对象一并进行手动注册。一一对应不同配置,再对应扫描不同数据源

@Configuration
@MapperScan(basePackages = "com.snow.datasource.dynamic.mybatis.mapper.r",sqlSessionFactoryRef="s1SqlSessionFactory")
public class RMybatisConfig {

    // 第一步,构建一个DataSource
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    // 第二步,构建一个SqlSessionFactory
    @Bean
    @Primary
    public SqlSessionFactory s1SqlSessionFactory() throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource2());
        return sessionFactory.getObject();
    }

    // 第三步,构建事务管理器
    @Bean
    public DataSourceTransactionManager s2TransactionManager(){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource2());
        return dataSourceTransactionManager;
    }
    
    @Bean
    public TransactionTemplate s1TransactionTemplate(){
        return new TransactionTemplate(s1TransactionManager());
    }

    @Bean
    public TransactionTemplate s2TransactionTemplate(){
        return new TransactionTemplate(s2TransactionManager());
    }
}

这样,在业务代码里,就可以通过引入不同的mapper来实现写入不同的数据源

public void sava1(Save1DTO dto){
    dto.setUuid = IdWorker.get32UUID();
    dto.setPath = "/pathS1";
    gen1Mapper.save(dto);
}

public void sava2(Save2DTO dto){
    dto.setUuid = IdWorker.get32UUID();
    dto.setPath = "/pathS2";
    gen2Mapper.save(dto);
}

但是,多数据源中,事务的情况怎么处理呢?

像以下代码的情况,会怎么样,虽然我加了@Transactional,思考一下?

只有抛异常,因为底层也不知道事务是哪个数据源

@Transactional(rollbackFor = Exception.class)
public boolean save(){
    Save1DTO dto = new Save1DTO();
    dto.setUuid = IdWorker.get32UUID();
    dto.setPath("/newPath");
    saveService.save(dto);
    int i = 1/0;
    return true;
}

那么如何解决事务的问题?

显而易见,我们必须得进到@Transactional这个注解里面,看看他源码是如何处理的。

没有指定事务管理器他走的默认的.....

所以,指定我们自己写的事务管理器就行

@Transactional(rollbackFor = Exception.class, value = "s1TransactionManager")
public boolean save(){
    Save1DTO dto = new Save1DTO();
    dto.setUuid = IdWorker.get32UUID();
    dto.setPath("/newPath");
    saveService.save(dto);
    int i = 1/0;
    return true;
}

再思考,一个方法中即有库1数据源,也有库2数据源,该如何处理事务?

idea直接提示报错

很简单,把一个方法拆成2个方法

  • 这不就是简单的实现了一个分布式事务嘛!大的不行就先拆成小的

思考🤔:这种方式有什么弊端?

是不是每次针对不同的数据源我们要写很多的配置尼 ?

三、使用dynamic-datasource框架

dynamic-datasource是MyBaits-plus作者设计的一个多数据源开源方案。使用这个框架非常简单,只需要引入对应的pom依赖

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.5.0</version>
</dependency>

这样就可以在SpringBoot的配置文件中直接配置多个数据源

这样就配置完成了masterslave_1两个数据库。
接下来在使用时,只要在对应的方法或者类上添加@DS注解(在方法上面、类上面都可以)即可。例如

@Service
public class GenServiceImpl {
    @Autowired
    private GenMapper genMapper;
    @DS("slave") // 从库, 如果按照下划线命名方式配置多个 , 可以指定前缀即可(组名)
    public List<SaveDTO> list() {
        return genMapper.list();
    }
    
   
    @DS("master")
    public void save(SaveDTO dto) {
        genMapper.save(dto);
    }
    
    @DS("master")
    @DSTransactional
    public void saveAll(){
        // 执行多数据源的操作
    }
}

思考🤔:这种操作非常简单,那这么做就完啦?

NO NO NO .....

盲猜,最大的问题,那么只能是:事务的问题!

对liao! 

虽然有这个注解,但是只是单数据源.....

如何在此场景下做多数据源的回滚?

查看源码的自动配置类分析:

拦截怎么做的呢?找他的invoke方法

来继续分析他底层源码:

进入notify之后,他内部做了一些转换,然后继续进notify方法

走到这一步,已是不易,核心就剩connection这个东西了。这个东西呢,也不是啥高级的,就是最基础的 java.sql下的,最基础jdbc操作里的

他的本质就是通过这个DataSouce去拿到这个connection。

如果我们做应该怎么做?

是不是应该拿到所有数据源的connection,然后来做回滚。思路都有了,接下来就不用再讲了吧。

四、针对多租户场景,如何做?

那么是不是可以从这个里面得到启发,核心就是,用户绑定不同数据源,那么,可不可以把用户和数据源之间的关联关系,保存下来,在进行切数据源操作的时候,匹配一下当前的用户

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值