⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

某中型电商平台的报表系统曾在深夜突然崩溃,起因竟是运营误删了共享表中的某租户数据列。

运维团队排查发现,因为缺乏有效租户隔离,一条误操作的ALTER TABLE语句导致全平台数据混乱。

这让我们警惕:选择多租户方案的每一步,都是安全与成本的权衡。

今天这篇文章就跟大家一起聊聊,多租户的4种常用方案,希望对你会有所帮助。

一、字段隔离方案

低成本背后的高风险

字段隔离方案,是通过统一数据表+租户ID过滤实现逻辑隔离。

如下图所示:

初期开发成本极低,但将数据安全的压力完全转移到了代码质量控制上。

致命缺陷检查清单:

  • 任意一次DAO层查询漏加tenant_id条件 → 数据跨租户泄露
  • 索引必须将tenant_id作为最左前缀 → 性能瓶颈风险
  • 全表扫描类查询(如报表统计)无法避免跨租户干扰

代码防御示范

(1)MyBatis拦截器自动注入租户ID

@Intercepts({@Signature(type = Executor.class, method = "update")})  
public class TenantInterceptor implements Interceptor {
public Object intercept(Invocation iv) throws SQLException {
MappedStatement ms = (MappedStatement) iv.getArgs()[0];
Object param = iv.getArgs()[1];

// 实体类自动填充tenant_id
if (param instanceof BaseTenantEntity) {
Field tenantIdField = param.getClass().getDeclaredField("tenantId");
tenantIdField.setAccessible(true);
if (tenantIdField.get(param) == null) {
tenantIdField.set(param, TenantContext.get());
}
}
return iv.proceed();
}
}

(2)SQL防火墙:强制全表扫描必须声明租户范围

/* 危险操作(可能扫全表) */  
SELECT * FROM orders WHERE status = 'PAID';


/* 安全写法(强制tenant_id过滤) */
SELECT * FROM orders
WHERE tenant_id = 'tenant_01'
AND status = 'PAID'
/* 必须添加LIMIT防止全量拉取 */
LIMIT 1000;

适用场景建议

  • 初期快速验证的MVP产品,用户量比较少的业务系统。
  • 对数据隔离要求较低的内部管理系统。

二、Schema隔离

数据库层的单元房

在同一个数据库实例中为每个租户独立Schema,实现库级别隔离。

如下图所示:

各租户表结构相同但数据独立,像小区里的不同住户单元。

运维警告清单:

  • 百级Schema数量级后,备份与迁移成本陡增
  • 跨Schema关联查询必须引入中间聚合层
  • 数据库连接池需按最大租户数配置 → 连接风暴风险

动态路由代码实现

(1)Spring动态数据源配置

spring:  
datasource:
dynamic:
primary: master
strict: true
datasource:
master:
url: jdbc:mysql://主库地址
tenant_001:
url: jdbc:mysql://从库地址?currentSchema=tenant_001
tenant_002:
url: jdbc:mysql://从库地址?currentSchema=tenant_002

(2)AOP切面动态切换Schema

@Aspect  
@Component
public class SchemaAspect {


@Before("@annotation(requireTenant)")
public void switchSchema(JoinPoint joinPoint) {
HttpServletRequest request = getCurrentRequest();
String tenantId = request.getHeader("X-Tenant-ID");

// 验证租户合法性
if (!tenantService.isValid(tenantId)) {
throw new IllegalTenantException("租户身份异常!");
}

// 动态切换数据源
DynamicDataSourceContextHolder.push(tenantId);
}


@After("@annotation(requireTenant)")
public void clearSchema() {
DynamicDataSourceContextHolder.clear();
}
}

适用场景建议

  • 需要中等安全级别的行业(教育、零售)。
  • 租户数<50且数据规模可控的系统。

三、独立数据库

数据隔离的终极形态

每个租户享有独立数据库实例。

如下图所示:

从存储到底层连接完全隔离。

安全性最高但成本呈线性增长。

财务预警清单:

  • 每个实例约增加¥3000/月(云RDS基础配置)
  • 跨租户数据聚合需额外ETL系统支持
  • DBA运维成本随租户数量直线上升

数据源动态路由核心代码

(1)抽象路由控制器

public class TenantDataSourceRouter extends AbstractRoutingDataSource {  


@Override
protected Object determineCurrentLookupKey() {
return TenantContextHolder.get();
}


@Override
protected DataSource determineTargetDataSource() {
String tenantId = (String) determineCurrentLookupKey();
DataSource ds = dataSourceMap.get(tenantId);
if (ds == null) {
ds = createNewDataSource(tenantId); // 动态创建新租户数据源
dataSourceMap.put(tenantId, ds);
}
return ds;
}
}

(2)多租户事务同步器(关键!)

@Bean  
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager() {
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
TenantDataSourceRouter router = (TenantDataSourceRouter) getDataSource();
router.initTenantDataSource(TenantContextHolder.get()); // 确保事务绑定正确数据源
super.doBegin(transaction, definition);
}
};
}

适用场景建议

  • 金融、医疗等强合规行业
  • 付费能力强且需要独立资源池的KA客户

四、混合架构

没有银弹的平衡术

核心原则:按租户等级提供不同隔离方案

在系统中创建租户时,根据租户的实际情况,给它分配一个等级。

不同的等级,使用不同的隔离方案。

如下图所示:

租户等级 隔离方案 资源配置
S级 独立数据库 独占RDS实例+只读副本
A级 Schema隔离 共享实例独立Schema
B级 字段过滤 共享表

动态策略选择器

针对不同的租户,我们可以使用策略模式,根据不同的等级,选择不同的数据库访问方式。

代码如下:

public class IsolationStrategyFactory {  


public IsolationStrategy getStrategy(String tenantId) {
TenantConfig config = configService.getConfig(tenantId);
switch(config.getLevel()) {
case VIP:
return new IndependentDBStrategy();
case STANDARD:
return new SchemaStrategy();
case BASIC:
default:
return new SharedTableStrategy();
}
}


// 示例策略接口
public interface IsolationStrategy {
DataSource getDataSource();
void executeQuery(String sql);
}
}

运维避坑必读

  1. 元数据管理:建立租户-资源映射表,避免配置漂移
  2. 迁移工具链:开发自动化升降级工具(如VIP客户从共享表迁移到独立库)
  3. 监控分层:不同方案的性能指标需独立采集分析

总结

这篇文章列举了多租户的4种常用方案。

没有最完美的,只有最合适的。

多租户设计的本质是资源、安全、成本的黄金三角博弈。

与其追求理论完美,不如根据业务阶段选择最适方案。

毕竟能用可控成本解决问题的,才是真正的架构智慧。

文章目录
  1. 1. 前言
  2. 2. 一、字段隔离方案
    1. 2.1. 低成本背后的高风险
    2. 2.2. 代码防御示范
  3. 3. 二、Schema隔离
    1. 3.1. 数据库层的单元房
    2. 3.2. 动态路由代码实现
  4. 4. 三、独立数据库
    1. 4.1. 数据隔离的终极形态
    2. 4.2. 数据源动态路由核心代码
  5. 5. 四、混合架构
    1. 5.1. 没有银弹的平衡术
    2. 5.2. 动态策略选择器
  6. 6. 总结