多租户方案的选型

在Spring Boot中实现多租户,核心是解决两个问题:如何识别请求来自哪个租户 ,以及如何根据租户信息隔离数据

业界主要有三种数据隔离方案,你可以根据业务需求选择:

方案 实现方式 优点 缺点 适用场景
独立数据库 每个租户拥有独立的物理数据库实例。 数据隔离级别最高,安全性最好。 成本最高,维护多个数据库实例、连接池和备份策略复杂。 金融、医疗等对数据安全要求极高的行业,或大企业客户。
独立Schema 共享同一个数据库实例,但为每个租户创建独立的Schema(数据库模式)。 隔离性较好,资源利用率比独立数据库高,运维成本相对较低。 需要管理多个Schema,跨Schema查询稍复杂。 中等规模、对数据隔离有较高要求的SaaS应用。
共享表(行级隔离) 所有租户共享相同的数据库和表,通过tenant_id字段来区分数据。 成本最低,资源利用率最高,实现简单。 隔离性弱,开发中容易遗漏tenant_id导致数据泄露;数据量大时性能可能下降。 初创SaaS、租户数量多但单租户数据量小的场景,或产品的免费/基础版本。

你也可以根据租户等级,组合使用多种方案,实现"混合隔离"。

核心实现步骤

无论选择哪种方案,实现步骤都大同小异。下面以最常用的"共享表+tenant_id"方案为例,演示核心实现。

1. 识别租户:从请求中获取租户ID

这是多租户的入口。你需要定义一个机制来获取每个请求的租户身份。

  • 常见方式 :从请求头(Header) (如 X-Tenant-ID)、JWT Token 中解析,或通过子域名 (如 tenant1.yourdomain.com)识别。
  • 代码实现 :编写一个HandlerInterceptor,在请求处理前拦截,提取租户ID并保存到线程上下文中。
java 复制代码
// 1. 定义租户上下文持有者,使用ThreadLocal保证线程隔离
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }

    public static String getTenantId() {
        return CURRENT_TENANT.get();
    }

    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

// 2. 编写拦截器,从请求头中提取租户ID并存入上下文
@Component
public class TenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 从请求头获取租户ID
        String tenantId = request.getHeader("X-Tenant-Id");
        // 可加入判空和默认值逻辑
        TenantContext.setTenantId(tenantId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求结束后清理上下文,防止内存泄漏
        TenantContext.clear();
    }
}
2. 隔离数据:根据租户ID过滤或路由

这是多租户的核心。根据你选择的方案,实现方式不同:

  • 对于"共享表"方案 :使用 MyBatis-Plus 的多租户插件 ,自动为所有SQL添加 tenant_id = ? 条件。
java 复制代码
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 从 TenantContext 中获取当前租户ID
                return new LongValue(TenantContext.getTenantId());
            }
            // ... 其他配置,如忽略某些表
        }));
        return interceptor;
    }
}
  • 对于"独立数据库/Schema"方案 :使用 AbstractRoutingDataSource 实现动态数据源。它能在运行时根据租户ID,动态切换到对应的数据源。
java 复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从 TenantContext 中获取当前租户ID作为路由键
        return TenantContext.getTenantId();
    }
}
3. 进阶:多线程环境下的租户传递

在异步任务或多线程环境中,ThreadLocal 无法自动传递。你需要使用 TransmittableThreadLocal 或配置 TaskDecorator 来确保租户信息不丢失。

三种方案对比与适用场景

对比维度 方案一:独立数据库 方案二:独立Schema 方案三:共享表
隔离级别 最高(物理隔离) 较高(逻辑隔离) 最低(逻辑/行级隔离)
数据安全性 极高,租户数据物理隔离,互不影响 ,Schema级隔离,权限清晰 ,依赖代码规范,容易因漏写tenant_id导致数据泄露
性能隔离 最好,租户独享数据库资源,无"噪音邻居"问题 较好,但同实例下仍可能因资源争抢相互影响 最差,一个租户的复杂查询可能耗尽整个库的资源,拖垮所有租户
硬件/运维成本 最高,每个租户需独立实例、连接池和备份策略 中等,共享数据库实例,但需管理多个Schema 最低,一套数据库即可服务所有租户
开发复杂度 中等,需要实现动态数据源路由 中等,同样需要动态数据源或Schema切换 最低,框架(如MyBatis-Plus)支持完善,易上手
租户定制能力 ,可为特定租户独立调整表结构、索引等 较强,可针对不同Schema进行定制 ,所有租户共享一套表结构,难以定制
适用场景 金融、医疗等强合规行业少量但体量大的大企业客户 中等规模SaaS;对数据隔离和性能有要求,但预算适中的场景 初创SaaS大量小型租户 ;产品免费/基础版,追求快速上线和低成本

决策框架

我们将通过思考下面四个关键问题,来逐步明确自己的选型方向。

  1. 数据安全和合规要求有多高?

    • 如果业务涉及金融、医疗、政务 等强监管行业,要求数据必须物理隔离,那么方案一(独立数据库) 是唯一选择。
    • 如果只是一般的商业数据,方案二(独立Schema) 的逻辑隔离通常已足够。
  2. 租户数量和规模是怎样的?

    • 租户数量少(如几十个)但每个数据量大方案一(独立数据库) 更合适,能提供最佳性能和隔离性。
    • 租户数量中等(如几百到上千)方案二(独立Schema) 是性价比较高的选择。
    • 租户数量巨大(数千至数万)且单个数据量小方案三(共享表) 成本最低,但需关注性能和隔离风险。
  3. 你的团队开发运维能力如何?

    • 方案一(独立数据库) 运维最复杂,需管理大量实例、连接池和备份。
    • 方案二(独立Schema) 运维中等,需处理Schema的创建、迁移和备份。
    • 方案三(共享表) 开发和运维最简单,能快速上手。
  4. 未来业务如何变化?

    • 架构设计要为未来留有余地。例如,可以先从方案三(共享表) 起步,当大客户出现时,为其单独升级为方案一(独立数据库)

趋势与最佳实践:混合模式

如今,混合模式 已成为一种主流实践。它允许你根据不同租户的等级和需求,采用不同的数据隔离方案。

  • 案例参考 :一个典型的SaaS平台可能会这样设计:
    • 免费或基础版租户 :使用 方案三(共享表),以最低成本提供基础服务。
    • 付费或高级版租户 :升级到 方案二(独立Schema),提供更好的性能和隔离性。
    • 企业级大客户 :采用 方案一(独立数据库),满足其最高的安全与性能要求。

这种模式能让你用一套代码和架构,服务好不同层次的客户。


总结与建议

  1. 方案选择是第一步 :根据你的安全需求、预算和租户规模,在三种方案中做出权衡。对于大多数SaaS应用,从"共享表"方案开始是一个成本较低的选择。
  2. 租户识别和上下文管理是基础 :使用拦截器(HandlerInterceptor)+ ThreadLocal 是管理租户上下文的经典组合。
  3. 善用框架能力 :MyBatis-Plus 的"多租户插件"和 Spring 的 AbstractRoutingDataSource 可以大幅简化你的实现工作。
  4. 安全是生命线 :在"共享表"方案中,必须确保所有查询都带上租户ID过滤,这是最需要警惕的地方。
  5. 考虑未来扩展:设计时考虑未来可能从"共享表"迁移到"独立Schema"或"独立数据库"的成本,保持架构的灵活性。
相关推荐
listening7778 小时前
HarmonyOS 6.1 跨设备数据库实战:分布式账本的落地与一致性校验
数据库·harmonyos·分布式账本
147API9 小时前
Claude Tag 进入 Slack 后,团队智能体需要哪些任务与审计字段
java·开发语言·数据库
Database_Cool_9 小时前
AI Agent 应用数据库选型:阿里云 PolarDB-X 高并发分布式数据底座
数据库·人工智能·阿里云
黄焖鸡能干四碗9 小时前
IT数据架构规划设计方案(PPT文件)
大数据·网络·数据库·人工智能·架构·区块链
黑夜路人9 小时前
可靠 Agent 设计:从一句 Prompt 到稳定交付
数据库·人工智能·prompt
IT瑞先生9 小时前
MariaDB与Mysql差异及版本对照
数据库·mysql·mariadb
草莓熊Lotso9 小时前
【Linux网络】深入理解Linux IO多路复用:从本质到select服务器实战
linux·运维·服务器·c语言·网络·数据库·c++
秋风渡.9 小时前
MySQL数据库操作
数据库·mysql
李燚10 小时前
LLM 可靠性设计:Retry + Failover 源码拆解(第58篇-E44)
数据库