多租户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);
相关推荐
小短腿的代码世界5 小时前
Qt对象树析构链与智能指针协同:零泄漏内存管理架构
开发语言·qt·架构
AI科技星5 小时前
数术江湖·全卷合集 - 硬核江湖・数理史诗
android·人工智能·架构·概率论·学习方法
John_ToDebug6 小时前
Chromium 132→148 升级实战:Legacy IPC 消息丢失问题深度解析
c++·chrome·ai·架构
恼书:-(空寄6 小时前
接口乱改直接炸线上!微服务接口版本控制全方案:URL_请求头版本+接口兼容原则,老旧系统无痛迭代
微服务·架构
happyprince6 小时前
08_verl-Workers模块详解
人工智能·架构·强化学习
丷丩7 小时前
错误处理与容错机制:GeoAI-UP的降级策略设计
架构·gis·容错设计
小短腿的代码世界7 小时前
Qt定时器高精度架构:从QTimer源码到纳秒级定时调度
数据库·qt·架构
手握风云-7 小时前
ProtoBuf:从序列化原理到高性能架构底座(一)
java·网络·架构
阿狸猿7 小时前
论大规模分布式系统缓存设计策略
架构
G_whang8 小时前
AgentMemory — 持久记忆系统:安装、架构与深度使用指南
ai·架构