多租户的 4 种常用方案!

关系型数据库NoSQL数据库数据安全

大家好,我是苏三,又跟大家见面了。

前言

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

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

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

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

一、字段隔离方案

低成本背后的高风险

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

如下图所示:picture.image

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

致命缺陷检查清单

  • 任意一次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,实现库级别隔离

如下图所示:picture.image各租户表结构相同但数据独立,像小区里的不同住户单元。

运维警告清单

  • 百级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且数据规模可控的系统。

三、独立数据库

数据隔离的终极形态

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

如下图所示:picture.image

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

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

财务预警清单

  • 每个实例约增加¥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种常用方案。

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

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

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

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

如果看了文章有些收获,记得给我点赞喔,谢谢你的支持和鼓励。

最后欢迎 加入苏三的星球 ,你将获得: 商城微服务实战、秒杀系统实战 、 商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

picture.image

目前星球已经更新了4900+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1600+小伙伴加入学习。

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动 NoSQL 的实践与探索
随着 NoSQL 的蓬勃发展越来越多的数据存储在了 NoSQL 系统中,并且 NoSQL 和 RDBMS 的界限越来越模糊,各种不同的专用 NoSQL 系统不停涌现,各具特色,形态不一。本次主要分享字节跳动内部和火山引擎 NoSQL 的实践,希望能够给大家一定的启发。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论