多租户SaaS架构

目录

一、基础概念

1. 什么是多租户SaaS系统

一套代码、一套部署环境,同时给多个企业/客户(租户) 使用,做到:

  • 租户之间数据互相隔离,互不干扰
  • 支持租户独立配置、权限、个性化功能
  • 降低部署、运维、服务器成本

2. 多租户核心诉求

数据安全隔离、租户权限管控、灵活扩容、低成本运维、租户定制化

二、三大数据隔离方案

隔离方案 核心原理 优点 缺点 适用租户 常用实现
1. 独立数据库 一个租户 = 单独一套数据库 隔离级别最高、数据最安全、故障互不影响、易备份迁移 成本高、库维护量大、租户多了服务器压力大 大型企业、付费大客户、涉密数据 动态数据源切换、多数据源管理
2. 共享库,独立Schema 共用一个数据库实例,每个租户单独Schema(MySQL对应不同库名) 隔离中等、成本适中、运维压力小、权限好管控 隔离弱于独立库、单库压力随租户上涨 中型企业、中小付费租户 MyBatis-Plus租户插件、Schema动态切换
3. 共享库,共享数据表 所有租户共用同一张表,加租户ID字段区分数据 成本最低、扩容最快、租户数量无上限 隔离最弱、数据泄露风险高、大数据量查询慢 小微企业、免费租户、轻量SaaS 全局拦截SQL自动拼接tenant_id

三、三种多租户隔离方案实现

运行环境:SpringBoot 2.7.x + MyBatis-Plus 3.5.x

公共前置依赖

xml 复制代码
<!-- 基础依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

方案一:独立数据库隔离(高隔离、大客户/涉密业务)

核心思路 :一个租户对应一套独立数据库,使用dynamic-datasource动态数据源组件,根据租户标识自动路由切换数据源。

1. 新增动态数据源依赖

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>

2. yml多数据源配置

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: default # 默认数据源
      datasource:
        # 租户1数据库
        tenant_1001:
          url: jdbc:mysql://127.0.0.1:3306/db_1001?useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.cj.Driver
        # 租户2数据库
        tenant_1002:
          url: jdbc:mysql://127.0.0.1:3306/db_1002?useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: 123456

3. 数据源切换使用

通过注解指定数据源,也可拦截器根据租户ID自动赋值切换

java 复制代码
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

    // 切换到1001租户独立库
    @DS("tenant_1001")
    public List<TenantUser> getTenant1User(){
        return userMapper.selectList(null);
    }

    // 切换到1002租户独立库
    @DS("tenant_1002")
    public List<TenantUser> getTenant2User(){
        return userMapper.selectList(null);
    }
}

4. 全局自动切换(进阶)

网关/拦截器解析Token中的租户ID,匹配数据源标识存入上下文,结合动态数据源路由策略,实现无注解自动切库。

方案二:共享数据库实例、独立Schema(中等隔离、中小企业常用)

核心思路:共用一台数据库服务,每个租户单独一个Schema(MySQL等价独立数据库),通过Mybatis拦截器在SQL执行前切换会话Schema,实现空间隔离。

1. Schema上下文工具类

java 复制代码
public class SchemaHolder {
    private static final ThreadLocal<String> SCHEMA_LOCAL = new ThreadLocal<>();

    public static void setSchema(String schemaName) {
        SCHEMA_LOCAL.set(schemaName);
    }
    public static String getSchema() {
        return SCHEMA_LOCAL.get();
    }
    public static void clear() {
        SCHEMA_LOCAL.remove();
    }
}

2. 自定义Mybatis拦截器切换Schema

java 复制代码
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaSwitchInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取当前租户对应的Schema库名
        String schema = SchemaHolder.getSchema();
        Connection conn = (Connection) invocation.getArgs()[0];
        // 切换当前数据库会话
        conn.setCatalog(schema);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

3. 拦截器注入配置

MybatisPlusConfig中添加拦截器即可,请求解析租户后存入Schema名称,自动切换数据库空间。

方案三:共享库共享表 + 租户ID隔离(低隔离、海量租户通用)

核心思路 :所有租户共用数据表,表新增tenant_id字段,通过MP内置租户拦截器自动拼接查询条件,业务代码无感知过滤数据。

1. 数据表设计

sql 复制代码
CREATE TABLE tenant_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    phone VARCHAR(20),
    tenant_id BIGINT COMMENT '租户唯一标识'
);

2. 租户上下文(线程隔离存储租户ID)

java 复制代码
public class TenantHolder {
    // ThreadLocal 保证单个请求线程租户数据隔离
    private static final ThreadLocal<Long> TENANT_THREAD_LOCAL = new ThreadLocal<>();

    // 设置当前租户
    public static void setTenantId(Long tenantId) {
        TENANT_THREAD_LOCAL.set(tenantId);
    }
    // 获取当前租户
    public static Long getTenantId() {
        return TENANT_THREAD_LOCAL.get();
    }
    // 请求结束清理,防止内存泄漏
    public static void clear() {
        TENANT_THREAD_LOCAL.remove();
    }
}

3. MP租户拦截器配置(核心)

java 复制代码
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 租户行级拦截器
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(
                // 动态获取当前线程租户ID
                TenantHolder::getTenantId
        );
        // 指定租户字段名
        tenantInterceptor.setTenantIdColumn("tenant_id");
        // 忽略无需租户隔离的公共表
        tenantInterceptor.setIgnoreTable("sys_config","sys_dict");
        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

4. 实体类

java 复制代码
@Data
public class TenantUser {
    private Long id;
    private String name;
    private String phone;
    private Long tenantId;
}

5. 使用效果

业务无需手动拼接租户条件,框架自动过滤

java 复制代码
// 自动生成 SQL:select * from tenant_user where tenant_id = 1001
List<TenantUser> userList = userMapper.selectList(null);
相关推荐
curd_boy15 小时前
【AI】生产级 Graph RAG 落地架构
人工智能·架构
解局易否结局16 小时前
从架构视角看 ops-transformer:一个解决分层系统设计问题的算子仓库
深度学习·架构·transformer
hz5678917 小时前
智慧政务视频会议系统技术架构解析:从场景需求到国产化落地的完整方案
架构·政务
生成论实验室17 小时前
通用人工智能(AGI)完整技术方案:以字序生命模型(WOLM)为认知内核的双脑协同架构
人工智能·语言模型·架构·创业创新·agi
刀法如飞18 小时前
DDD 与 Ontology 对比分析:哪一种更适合AI时代复杂系统构建?
java·架构·领域驱动设计
2601_9545267518 小时前
底层架构与并发性能:多态胶原饮“竞品对比”的技术评估报告
架构
5008419 小时前
Conv + BN + ReLU 融合:省掉两次显存读写
flutter·架构·开源·wpf·音视频
计算机魔术师21 小时前
【AI面试八股文 Vol.3.4:训练微调部署选型】从预训练到量化部署:LLM 工程落地如何做模型选择
人工智能·后端·面试·架构·moe·vol.3.3·vol.3.4