spring+Druid+mybatils多租户下动态切换数据源

什么是多租户

举个例子:蔡徐坤、展亚鹏和范小勤三个人去租房子,他们因为家里经济困难所以勤工俭学,三个人决定合租一套三室一厅的房子,虽然每个人有自己的房间,但是家里的水电、厨房、卫生间和热水器都是大家一起公用的。隐私性肯定是没有单独自己租房子来的高。

在多租户的架构里,多个租户共享相同的服务器、基础设施,数据库可以是共享的也可以是隔离的,由于多租户必定在用户规模上比单租户来的大,所以多租户一般会有多个实例,共用一套实例代码。租户之间的数据隔离往往采用逻辑隔离的方式,即在代码和数据库层面隔离,所以安全性远没有单租户来的高。

就比如上面举的例子,虽然三人都租有自己单独的房间,但房子里的的厨房、卫生间和洗衣机都是大家一起公用的。从方便和隐私的角度来看,都不如自己一个房子好。

在系统中,多租户体现为,多个租户共用一个或多个服务器、基础设施,数据库可以是共享也可以是隔离的,多个租户共用一套代码,或者在微服务中共用一个或者几个模块,租户和租户之间实现数据的隔离,但是安全性远不如单租户。但是其维护、修改成本都比单租户更低,因此如果系统是对安全性要求不这么高、定制性不这么强的系统,多租户是很好的一个方案。但对于一些大型网站、或者安全性需求强的网站,最好还是不用多租户。大厂的项目更多还是定制化开发,而中小厂为了节约成本可能会采用多租户。

多租户数据隔离的实现方式

基于字段隔离

在每一个表上都添加上租户id,所有数据都在一个库,查询时动态拼接租户id到sql。

优点:开发成本低,添加租户不需要做额外逻辑,跨租户逻辑简单

缺点:隔离程度最低,安全性最差,维护成本高,各租户数据耦合严重,维护成本高,每次的sql语句都需要拼接租户id,每个租户的数据量不能过大(可以后期分库分表)

基于表隔离

在表名上添加对应的租户信息,或使用视图进行数据过滤

优点:开发成本较低,隔离性相对较好,可以拥有相对较大的数据量

缺点:跨租户逻辑复杂,维护成本相对较高

基于库隔离

优点:隔离性最强,安全性最高,后期维护或者新增需求需要成本较小,灵活性更高

缺点:开发成本大,跨租户统计困难,新增租户时逻辑较复杂

基于mybatis实现基于库的数据分离

依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
       <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version> <!-- 根据你的实际情况选择版本号 -->
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.6</version>
        </dependency>

实体类:

对应数据库表,存放数据库的基本信息和租户id,可以通过驱动类型实现不同数据源使用不同的数据库,例如a使用mysql,b使用postgresql

package com.zy.saas.domian;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author: Larry
 * @Date: 2024 /01 /28 / 2:58
 * @Description:
 */
@Data
@Builder
@Accessors(chain = true)
@TableName("datasource")
public class Datasource {
    private Integer id;

    /**
     * 数据库地址
     */
    private String url;
    /**
     * 数据库用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 数据库驱动
     */
    private String driverClassName;
    /**
     * 数据库key,即保存Map中的key
     */
    private String name;
    /**
     * 租户id
     */
    private Integer tenantId;
}

核心类:

存放当前用户数据库url的线程变量

package com.zy.saas.Context;

/**
 * @author: Larry
 * @description:
 **/
public class DataSourceContextHolder {
    //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
    private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     * @param dataSourceName 数据源名称
     */
    public static void setDataSource(String dataSourceName){
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /**
     * 获取当前线程的数据源
     * @return 数据源名称
     */
    public static String getDataSource(){
        return DATASOURCE_HOLDER.get();
    }

    /**
     * 删除当前数据源
     */
    public static void removeDataSource(){
        DATASOURCE_HOLDER.remove();
    }

}

 将主库的数据源信息根据yml导入,即默认数据源为yml里面配置的数据源

package com.zy.saas.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.zy.saas.DynamicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: Larry
 * @description: 设置数据源
 **/
@Configuration
public class DateSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master",defaultDataSource);
        return new DynamicDataSource(defaultDataSource,dataSourceMap);
    }

}

  DynamicDataSource(动态数据源)是指在应用程序中根据需要动态切换数据源的机制。

  通过这个类实现了对所有数据库信息的校验,保存。

package com.zy.saas;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.zy.saas.Context.DataSourceContextHolder;
import com.zy.saas.domian.Datasource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @author: Larry
 * @description: 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
 **/

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    private final Map<Object,Object> targetDataSourceMap;

    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
        this.targetDataSourceMap = targetDataSources;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }

    /**
     * 添加数据源信息
     *
     * @param dataSources 数据源实体集合
     */
    public void createDataSource(List<Datasource> dataSources){
        try {
            if (CollectionUtils.isNotEmpty(dataSources)){
                for (Datasource ds : dataSources) {
                    //校验数据库是否可以连接
                    Class.forName(ds.getDriverClassName());
                    DriverManager.getConnection(ds.getUrl(),ds.getUsername(),ds.getPassword());
                    //定义数据源
                    DruidDataSource dataSource = new DruidDataSource();
                    BeanUtils.copyProperties(ds,dataSource);
                    //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
                    dataSource.setTestOnBorrow(true);
                    //建议配置为true,不影响性能,并且保证安全性。
                    //申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
                    dataSource.setTestWhileIdle(true);
                    dataSource.init();
                    this.targetDataSourceMap.put(ds.getName(),dataSource);
                }
                super.setTargetDataSources(this.targetDataSourceMap);
                // 将TargetDataSources中的连接信息放入resolvedDataSources管理
                super.afterPropertiesSet();
            }
        }catch (ClassNotFoundException | SQLException e) {
            log.error("---程序报错---:{}", e.getMessage());
        }
    }

    /**
     * 校验数据源是否存在
     * @param key 数据源保存的key
     * @return 返回结果,true:存在,false:不存在
     */
    public boolean existsDataSource(String key){
        return Objects.nonNull(this.targetDataSourceMap.get(key));
    }
}

  spring监听器,在spring后启动时自动触发一次,调用DynamicDataSource将数据源信息添加到

  targetDataSourceMap里面

package com.zy.saas.config;
import com.zy.saas.DynamicDataSource;
import com.zy.saas.Mapper.DataSourceMapper;
import com.zy.saas.Service.UserService;
import com.zy.saas.domian.Datasource;
import com.zy.saas.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;

/**
 *
 * @author Larry
 */
@Component
@Slf4j
public class ContentRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {

    @Resource
    private DynamicDataSource dynamicDataSource;
    @Resource
    private DataSourceMapper dataSourceMapper;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        List<Datasource> dataourceList = dataSourceMapper.getListAll();
        System.out.println(dataourceList);
        if (!CollectionUtils.isEmpty(dataourceList)) {
               dynamicDataSource.createDataSource(dataourceList);
        }
    }
}

 主要思路

首先项目启动后,将所有租户的数据源信息通过Listener调用一次DynamicDataSource的createDataSource方法,将数据源信息存储到targetDataSourceMap里面,登陆时拦截器首先判断用户具体属于哪一个租户,获取租户id后,根据租户id判断出其所属的数据源,然后调用线程变量的set方法实现切换(默认数据源是在配置类里面配置的主库),当切换完成后调用remove防止不同线程变量出现访问错误。

注意事项

启动类上需要加上

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

否则会报循环依赖,如下

a4297472685d482ea051293c9cca450b.png

原因是org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration会引入一个Registrar注册了一个后置处理器,这个注册过程其实org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry中完成的。这个后置处理器会在所有DataSource类型的Bean的初始化后进行处理,此时会去获取DataSourceInitializerInvoker类型的bean.而这个DataSourceInitializerInvoker类型的bean又会依赖DataSource,导致循环依赖。而这个bean其实作用是执行一些脚本的,可以不要,注册一个BeanDefinitionRegistryPostProcessor移除对应的后置处理器,这样在数据源初始化的时候就不会去获取DataSourceInitializerInvoker了。

这个当时卡了我很长时间

结语

本人能力有限,实现方式可能不是最优方法,希望有更好的方法的大佬可以在评论区提出来,大家发现我的错误或者有疑问的地方,可以在评论区@我

 

  • 34
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Boot是一个用于构建独立的、可执行的Spring应用程序的框架,简化了Spring应用程序的配置和部署。JPA(Java Persistence API)是一种用于管理Java对象和关系数据库之间映射的规范。Druid是阿里巴巴开源的关系型数据库连接池。 在Spring Boot中配置多数据源需要以下几步: 1. 引入相关依赖:需要引入Spring Boot、Spring Data JPA和Druid的相关依赖。 2. 配置数据源:在application.properties或application.yml文件中配置多个数据源的连接信息,并指定每个数据源的名称和相关属性。 3. 配置数据源连接池:使用@ConfigurationProperties注解创建多个数据源的连接池对象,并指定数据源的名称以及相关属性。 4. 配置实体管理器工厂:为每个数据源配置对应的实体管理器工厂,用于处理JPA实体与数据库之间的映射关系。 5. 配置事务管理器:为每个数据源配置对应的事务管理器,用于处理事务操作。 6. 配置数据源路由:创建动态数据源,根据传入的数据源名称选择对应的数据源进行操作。 7. 配置JPA的Repository:创建接口继承JpaRepository,用于定义数据访问方法。 通过以上步骤配置好多数据源后,就可以在Spring Boot应用程序中使用多个数据源进行数据库的操作。可以根据需要在Service或Controller中使用@PersistenceContext注解指定具体的数据源,或者使用@Primary注解指定默认的数据源。 总结:通过Spring Boot的自动配置和Druid的连接池,可以很方便地实现多数据源的配置。使用JPA进行数据操作,能够有效地减少开发人员编写SQL语句的工作量,提高开发效率。通过合理的配置,可以根据需要选择不同的数据源进行操作,实现灵活的数据访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值