Multi-Tenant SaaS Architecture: 2026 年完整指南

如何设计多租户 SaaS 架构——隔离模型、租户识别、行级作用域、跨租户分析、迁移——基于 9 款生产应用验证。

Huifer
Huifer
July 14, 202611 min read
其他语言:Deutsch · English

本文由 Huifer 撰写——TanStack Ship 的独立开发者兼维护者。在九款生产环境多租户 SaaS 应用中——包括一款日均约 14 万请求的多租户分析工具、两个 B2B 计费平台、一个工作区协作工具,以及五款小型应用——我交付过、搞砸过、重新交付过那些决定租户边界能否在真实流量下承受住考验的模式。这份指南整合了这些模式:隔离模型、租户识别、行级作用域、跨租户分析和迁移路径。下面的每个模式都已在交付的代码中实现。精简版见多租户架构指南。

验证来源:Cloudflare D1 文档 · Cloudflare Workers 文档 · SQLite 查询规划器 · TanStack Router 文档 · TanStack Ship GitHub 组织 · TanStack Ship 多租户参考仓库

最后更新:2026-07-14 · 更新日志


TL;DR: 多租户 SaaS 架构的核心是在共享基础设施的同时隔离客户数据。对于约 95% 的 SaaS 产品,正确模型是行级隔离:一个数据库、一个 Schema,每张表以 tenant_id 列开头,租户上下文在边缘层注入。本指南深入讲解三种隔离模型、租户识别、防止数据泄漏的行级规范、跨租户分析,以及迁移路径。如果你在选型,参见 TanStack Ship 功能页;更广泛的背景见 SaaS 架构 2026 指南。


三种隔离模型及各自的适用场景

诚实的建议:大多数应用选行级

每个多租户决策都从三个选项开始:每租户独立数据库、每租户独立 Schema,或共享表加 tenant_id 列。很容易一开始就选隔离程度最高的方案,因为感觉更安全。不要这样做。隔离程度最高的方案运维成本也最高,迁移最慢,在规模下最难理清。Cloudflare D1 文档指出 D1 支持每个账户多个数据库;问题是你是否需要在第一天就承受这种复杂性。

我为约 95% 的 SaaS 产品交付的模型是行级隔离:一个数据库、一个 Schema,每个领域表以 tenant_id 开头,每个查询按租户过滤,租户上下文在边缘层注入后才运行任何服务端函数。一个数据库意味着一个连接、一次迁移、一份备份。一个 Schema 意味着 EXPLAIN 对每个租户行为一致。一套表意味着跨租户分析只是一条 SQL 查询。另外两种模型有其合理用途——受监管行业、要求物理隔离的企业客户、数据量远超他人的大租户。去这两种模型的路径已有大量先例;回来的路径却很痛苦。先从简单出发。

何时值得用每租户独立数据库

每租户独立数据库为每个租户分配独立的 D1 数据库,应用基于租户上下文路由请求。优势:物理隔离、按租户备份和恢复、可为大租户配置独立读副本拓扑。劣势:每增加一个数据库就多一个绑定、一次迁移、一条计费项。我交付过的合理场景:一个医疗平台,某客户要求符合 HIPAA 的按租户数据驻留;一个 B2B 平台,某客户产生了约 40% 的写入量(拆分该客户后降低了所有人的写锁竞争);一个白标平台,每个实例作为独立产品销售。如果以上场景都不适用,不要采用——开销随租户数量线性增长。

Schema-per-tenant 作为中间方案,却很少值得使用

每租户独立 Schema 的吸引力在于:物理隔离、一个连接、一次迁移。但在 SQLite(或 D1)中,Schema-per-tenant 无法提供 Postgres 用户所期望的能力。查询规划器不会像跨表那样跨 Schema 优化;跨 Schema 连接很慢;部分索引无法迁移。Schema-per-tenant 在有成熟 search_path 切换经验的 Postgres 后端应用中才真正值得——那是另一个技术栈。本指南其余部分假设行级隔离;相关模式可直接迁移到 Schema-per-tenant(将 tenant_id 替换为 schema_name)和每租户独立数据库(将 tenant_id 替换为 database_name)。

租户识别:三种模式,一条规则

唯一规则:在任何业务逻辑运行前解析租户

第一个架构决策是请求如何识别其租户。第二个——也更重要的——是何时发生该识别。我交付时遵循的唯一规则是:在任何业务逻辑运行前解析租户。 租户上下文作为环境状态流经整个请求;每个服务端函数、每个 loader、每个 webhook 处理器直接读取它,无需重新派生。三种识别模式覆盖约 99% 的 SaaS 应用:子域名(acme.example.com —— host header 携带 slug)、路径前缀(example.com/t/acme/dashboard —— 第一个路径段携带 slug,TanStack Router 的嵌套布局使上下文对每个嵌套路由可用)和 auth token(租户存在于 session 中,适用于用户属于单一工作区的 B2B 应用)。子域名最干净,因为每个请求本身就是一个租户请求。路径前缀最灵活,因为它在单一域名上即可工作。auth token 是白标应用的兜底方案。

边缘层的子域名解析

Cloudflare Workers 上的模式解析 host header 并在 tenants 查找表中查找,返回租户作为环境上下文:

typescript
// src/middleware/tenant.ts
export const tenantMiddleware = defineMiddleware({
  before: async ({ request }) => {
    const host = request.headers.get('host') ?? ''
    const slug = host.split('.')[0]
    if (['www', 'api', 'app'].includes(slug)) return
    const tenant = await db
      .prepare('SELECT id, plan, status FROM tenants WHERE slug = ?')
      .bind(slug).first<Tenant>()
    if (!tenant || tenant.status !== 'active') {
      return new Response('Tenant not found', { status: 404 })
    }
    return { tenantId: tenant.id, tenantPlan: tenant.plan }
  },
})

中间件在每个服务端函数之前运行;返回的对象成为环境上下文——每个 loader、mutation 和 webhook 处理器直接读取 context.tenantId,无需二次查询。TanStack Router 自动获取它。

路径前缀解析配合路由守卫

当子域名不可行时——本地开发、单一预发布环境、路径优先的产品——路径前缀模式使用 TanStack Router 的嵌套布局。routes/t/$tenantSlug/ 下的 _layout.tsx 解析 slug,未命中时重定向,返回租户上下文。$tenantSlug 段是路由参数,不是查询字符串——它能经受页面刷新并可分享。TanStack Router 高级模式指南 涵盖了相关约定。

行级隔离:四项规范

每个领域表以 tenant_id 开头

第一项规范是机械性的:每个领域表的第一列是 tenant_id,每个复合索引的第一列是 tenant_id,每个外键引用回租户。原因在于 SQLite 查询规划器的最左前缀规则:复合索引 (tenant_id, created_at) 为 WHERE tenant_id = ? ORDER BY created_at DESC 提供服务,无需排序。SQLite 查询规划器文档 指出,规划器高效使用该索引,对不含 tenant_id 的查询则忽略它——这正是每个查询都必须按租户过滤的数据库所需的行为。archived_at IS NULL 上的部分索引是 SQLite 特有的优化,使索引随表增长保持精简。完整的 D1 Schema 设计手册见 D1 生产指南。

每个查询都带作用域,类型系统强制执行

第二项规范真正防止了数据泄漏:每个查询都按租户过滤,无作用域的查询在编译时报错。模式是一个薄封装,接收 tenantId 作为必需参数,返回已将 tenant_id 嵌入每个子句的查询构建器:

typescript
// src/lib/tenant-db.ts
export function tenantDb(tenantId: string) {
  return {
    projects: {
      list: () => db.prepare(
        'SELECT * FROM projects WHERE tenant_id = ? AND archived_at IS NULL ORDER BY created_at DESC'
      ).bind(tenantId),
      insert: (name: string) => db.prepare(
        'INSERT INTO projects (id, tenant_id, name) VALUES (?, ?, ?)'
      ).bind(crypto.randomUUID(), tenantId, name),
    },
  }
}
// 每个服务端函数调用 tenantDb(context.tenantId)。
// 没有全局的 db.projects —— 只有带作用域的封装。

编译时好处:没有会遗忘的全局 db.projects。运行时好处:到达数据库的每个查询都已带有租户过滤。类型系统 + 环境上下文 + 无未封装全局——这个组合使行级隔离在大规模下安全。

软删除配合审计表

硬删除是个坑:用户点击"删除项目",行消失了,下一张发票仍引用该项目 ID,生产环境中触发外键违规。解决方案是软删除加审计表——每次删除在 db.batch() 内将记录插入 project_deletions 并更新 projects.deleted_at;读取时过滤 deleted_at IS NULL;每夜 cron 在导出到 R2 后硬删除超过 90 天的行。完整的 D1 迁移手册见 D1 深度指南。

速率限制、配额和计划边界按租户作用域

第四项规范防止最尴尬的生产事故:速率限制按租户而非按 IP 作用域。一个租户可以运行一千个并发同步而不会触发全局限制;吵闹的邻居无法耗尽共享桶从而饿死其他租户。速率限制 key 是 rl:${tenantId}:${route},每个租户拥有自己的桶。计划边界——free 60 req/min、pro 600 req/min、enterprise 不限——从环境上下文中的 tenantPlan 读取。计划变更无需代码变更;计划存在于数据库中。完整的 D1 运维手册见 D1 生产指南。

跨租户问题:一个数据库中更容易的工作

分析、计费,以及需要所有租户数据的查询

在行级隔离中确实更容易的工作是跨租户分析。一条 SQL 查询跨每个租户连接;无需扇出、无需按租户的任务、无需聚合管道。月度经常性收入查询是对已有索引执行 GROUP BY plan 的简单查询;无需特殊的分析管道、无需独立仓库、无需 ETL 任务。完整的可观测性形态见 SaaS 监控与可观测性指南。

管理员超级用户模式:独立的认证边界

需要谨慎处理的工作是管理员超级用户模式:登录一次即能查看所有租户的运营者。将管理员视为独立的认证边界,而非特殊租户。管理员服务端函数绕过 tenantDb 封装,直接调用原始 db;每个管理员操作都记录 actor: 'admin:<user_id>' 用于审计。添加 id 为 * 的"超级租户"的冲动是个坑——它打破了每个查询都带作用域的规范,第一次有人忘记过滤时,每个租户的数据都会泄漏到管理员视图。管理员路由导入原始 db,每条查询都写显式 WHERE tenant_id = ? 子句。

迁移:从简单形态走出的路径

从单租户到多租户的迁移

我交付过两次的路径:添加 tenants 表,插入原始客户并赋予稳定 id 如 original;在每个领域表添加 tenant_id,用 original 回填;在每个服务端函数外层封装 tenantDb 封装,id 从配置标志读取;构建租户注册流程,新客户获得新的 id;将原始 id 重命名为真实 slug,作为一次性迁移。中型应用约两周。两次犯过的错误是试图跳过封装步骤——"仅在迁移期间"保留未封装的 db 全局。每个存活下来的未封装全局都是未来的数据泄漏。

当租户超出共享形态时拆分到每租户独立数据库

第二次迁移是拆分:某租户的数据量远超他人,写锁竞争伤害了共享数据库。将该租户的行复制到新数据库,启动独立的 D1 绑定,在中间件层将对该租户的请求路由到新数据库。约一个周末的工作量;逆向操作——回到一个数据库——则需要重写。运维经验:从第一天起就为可拆分性设计路由层,即使当前没有租户需要拆分。所有数据库访问都通过 getDb(tenantId) 函数;每个服务端函数调用 getDb(context.tenantId) 而非导入全局。当拆分变得必要时,只需改配置,不需要重写。

本指南的边界

这就是 2026 年多租户 SaaS 架构的形态:约 95% 的应用使用行级隔离,租户识别在边缘层,四项规范防止数据泄漏,跨租户分析零额外成本,从一个租户到一千个租户的迁移路径。形态是持久的;底层原语是可以替换的。将 D1 换成 Postgres,行级规范可以迁移。将 Workers 换成 Node 服务端,中间件模式可以迁移。业务层面——定价、企业合同、 onboarding、合规——不在本指南范围内。


结尾 CTA: TanStack Ship 在九款 SaaS 应用中落地了本指南的每个多租户模式。查看功能页,与替代方案对比,或阅读 SaaS 架构 2026 指南 获取更广泛背景。D1 深度指南 涵盖数据层;TanStack Router 高级模式 讲解路由形态。