目录
- 一、基础概念
-
- [1. 什么是多租户SaaS系统](#1. 什么是多租户SaaS系统)
- [2. 多租户核心诉求](#2. 多租户核心诉求)
- 二、三大数据隔离方案
- 三、三种多租户隔离方案实现
-
- 公共前置依赖
- 方案一:独立数据库隔离(高隔离、大客户/涉密业务)
-
- [1. 新增动态数据源依赖](#1. 新增动态数据源依赖)
- [2. yml多数据源配置](#2. yml多数据源配置)
- [3. 数据源切换使用](#3. 数据源切换使用)
- [4. 全局自动切换(进阶)](#4. 全局自动切换(进阶))
- 方案二:共享数据库实例、独立Schema(中等隔离、中小企业常用)
-
- [1. Schema上下文工具类](#1. Schema上下文工具类)
- [2. 自定义Mybatis拦截器切换Schema](#2. 自定义Mybatis拦截器切换Schema)
- [3. 拦截器注入配置](#3. 拦截器注入配置)
- [方案三:共享库共享表 + 租户ID隔离(低隔离、海量租户通用)](#方案三:共享库共享表 + 租户ID隔离(低隔离、海量租户通用))
-
- [1. 数据表设计](#1. 数据表设计)
- [2. 租户上下文(线程隔离存储租户ID)](#2. 租户上下文(线程隔离存储租户ID))
- [3. MP租户拦截器配置(核心)](#3. MP租户拦截器配置(核心))
- [4. 实体类](#4. 实体类)
- [5. 使用效果](#5. 使用效果)
一、基础概念
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);