掘金 后端 ( ) • 2024-04-09 21:45

@[TOC]

一、背景

1、多租户概念

多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性。

多租户简单来说是指一个单独的实例可以为多个组织服务。多租户技术为共用的数据中心内如何以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍然可以保障客户的数据隔离。一个支持多租户技术的系统需要在设计上对它的数据和配置进行虚拟分区,从而使系统的每个租户或称组织都能够使用一个单独的系统实例,并且每个租户都可以根据自己的需求对租用的系统实例进行个性化配置。

多租户技术可以实现多个租户之间共享系统实例,同时又可以实现租户的系统实例的个性化定制。通过使用多租户技术可以保证系统共性的部分被共享,个性的部分被单独隔离。通过在多个租户之间的资源复用,运营管理维护资源,有效节省开发应用的成本。而且,在租户之间共享应用程序的单个实例,可以实现当应用程序升级时,所有租户可以同时升级。同时,因为多个租户共享一份系统的核心代码,因此当系统升级时,只需要升级相同的核心代码即可。

2、数据隔离

使用数据库级别的数据隔离是实现多租户架构的一种常见方式。可以为每个租户创建单独的数据库,或者使用数据库中的分表或分库来实现数据隔离。在 Spring Boot 中,可以使用多数据源配置来管理不同租户的数据库连接。

数据隔离是指在多租户架构中,保持各个租户之间的数据相互独立,确保一个租户的数据不会被其他租户访问或篡改。常用的方式是通过数据库级别的数据隔离来实现,即为每个租户创建独立的数据库、分表、分库等。

二、SpringBoot多数据源示例

1、导包

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!-- mysql连接驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-core</artifactId>
        <version>3.0.5</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

2、application.yml配置

server:
  port: 8081

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #自定义第一个数据源
    datasource1:
      url: jdbc:mysql://192.168.56.10:3306/test1?serverTimezone=UTC
      username: root
      password: root
      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://192.168.56.10:3306/test2?serverTimezone=UTC
      username: root
      password: root
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver

3、动态数据源配置

import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

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

/**
 * @desc 使用AbstractRoutingDataSource创建两个库
 */
@Component("dynamicDataSource")
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {

    public static ThreadLocal<String> name = new ThreadLocal<>();
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    @Resource(name = "dataSource1")
    DataSource dataSource1;
    @Resource(name = "dataSource2")
    DataSource dataSource2;

    @Override
    public void afterPropertiesSet() {
        // 为targetDataSources初始化所有数据源
        Map<Object, Object> targetDataSources=new HashMap<>();
        targetDataSources.put("d1",dataSource1);
        targetDataSources.put("d2",dataSource2);

        super.setTargetDataSources(targetDataSources);

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

        super.afterPropertiesSet();
    }
}

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.apache.ibatis.session.LocalCacheScope;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.io.IOException;

@Configuration
@MapperScan("com.routingDs.mapper")
public class DataSourceConfig {

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

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

    /* @Bean
    public Interceptor dynamicDataSourcePlugin(){
        return new DynamicDataSourcePlugin();
    }
*/

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


    @Bean
    public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DynamicDataSource dataSource) throws Exception {
        return sqlSessionFactoryBean(dataSource, "classpath:mapper/*.xml").getObject();
    }

    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource, String mapperLocation) throws IOException {
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();

        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();

        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);

        sessionFactoryBean.setConfiguration(configuration);

        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocation));


        return sessionFactoryBean;
    }
}

4、编写mybatis信息

@Repository
public interface CourseMapper {


    @Insert("insert into course(id, name, content) values(#{id}, #{name}, #{content})")
    int insertOne(Course course);

    @Select("select * from course")
    List<Course> select();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.roy.routingDs.mapper.CourseMapper">
</mapper>

5、测试

@Controller
@RequestMapping("/RDS")
public class CourseControllerRDS {
    @Resource
    CourseMapper courseMapper;

    @ResponseBody
    @RequestMapping("/queryCourse")
    public Object queryOrder(@RequestParam(value = "dsKey",defaultValue = "d1") String dsKey){
        DynamicDataSource.name.set(dsKey);
        return courseMapper.select();
    }

    @ResponseBody
    @RequestMapping("/createCourse")
    public String createCourse(@RequestParam(value = "dsKey",defaultValue = "d2") String dsKey, Course course){
        DynamicDataSource.name.set(dsKey);
        courseMapper.insertOne(course);
        return "SUCCESS BY RDS";
    }
}

6、结果

我们发现,可以通过参数,进行动态的控制数据源。

进一步我们可以总结出,可以通过当前登录的用户session,来获取到该用户的租户信息,并通过租户信息来获取到该租户所持有的数据源,来实现多租户的数据隔离

三、实现多租户的数据隔离

设想待实现: 1、用户登录之后,在session中存放租户id,用于区分用户的租户。 2、在获取数据源时,通过租户id动态获取数据源,从而实现多租户的数据源隔离。 3、创建租户、删除租户时,同步创建一个数据库或者删除一个数据库。 4、租户的数据源信息可以存放在mysql等地方,系统启动自动加载。