如何设计一个支持多租户的后端系统
结论:多租户后端设计的第一决策不是技术选型,而是数据隔离级别——默认选「共享库 + 共享表 + 强制 tenant_id」,只有强合规行业(金融、医疗)或 Top 5% 的大客户才升级到独立 schema 或独立库;定完隔离级别,再依次解决 tenant_id 强制注入、租户上下文透传、按租户限流分片、租户生命周期运维这四件事。
多租户的三种隔离模式怎么选?
结论:共享库共享表能覆盖 90% 的 SaaS 场景,独立 schema 适合几百到几千个中大型租户,独立库/独立实例只留给愿意单独付费的大客户。
租户(tenant)指共用同一套系统、但数据必须互相看不见的一组客户组织。共享库共享表是全部租户的表结构相同、数据混存,靠 tenant_id 列区分,成本最低,单库可承载百万级租户;代价是任何一条 SQL 漏写 tenant_id 就是越权事故。共享库独立 schema 是每个租户一套表,PostgreSQL 里执行 CREATE SCHEMA tenant_123; 即可,隔离性比共享表强,但当 schema 数量超过 1000 个,DDL 批量执行和备份恢复时间会明显变长。独立库或独立实例隔离最彻底,成本也最高。
常见落地是混合模式:95% 的中小租户跑共享库,头部客户走独立库,代码层通过一个 TenantDataSource 路由接口统一抽象,业务代码不感知差异。
tenant_id 怎么保证不被漏写?
结论:不靠开发者自觉,靠数据库层强制,漏写就报错而不是返回别人的数据。
PostgreSQL 直接开 RLS(Row Level Security,行级安全):先 ALTER TABLE orders ENABLE ROW LEVEL SECURITY;,再建策略 CREATE POLICY p_orders ON orders USING (tenant_id = current_setting('app.tenant_id')::bigint);,每个请求开始时执行 SET LOCAL app.tenant_id = '42';。这样即使业务 SQL 写错,也只会查到本租户的行。
MySQL 没有原生 RLS,替代方案是 MyBatis 拦截器或 ORM 中间件统一给 SQL 追加 AND tenant_id = ?,配合审计日志对没带 tenant_id 的查询告警。
索引必须带租户维度:唯一索引用 UNIQUE (tenant_id, order_no) 而不是 UNIQUE (order_no),否则租户 A 的单号会占用租户 B 的命名空间;外键同样用复合键 (tenant_id, id)。
租户上下文怎么在请求链路里传递?
结论:入口解析、链路透传、出口清理,三步缺一不可,线程池复用是串租户的高发点。
入口层从 JWT 的 tenant_id claim、子域名(acme.example.com)或 X-Tenant-Id 请求头解析出租户,并且必须校验「当前登录用户确实属于这个租户」,否则改一个 Header 就能读到别家数据。
透传方式按语言选:Java 用 ThreadLocal 或 TransmittableThreadLocal,Go 用 context.Context,Node.js 用 AsyncLocalStorage。关键点是线程池和协程复用场景下必须在 try/finally 里清空上下文,否则上一个请求的租户会泄漏到下一个请求。
异步链路同样要带:MQ 消息体里写入 tenant_id,消费者先设置上下文再处理;定时任务按租户分批执行而不是一次性扫全表。缓存 key 一律带租户前缀,例如 t:42:user:1001,绝不能出现 user:1001 这种全局 key。
大租户和长尾租户怎么一起跑?
结论:配额、限流、分片全部按租户维度做,避免单个租户把整站拖垮。
限流用 Redis 令牌桶,key 带租户 ID,按套餐给不同额度,比如免费版 50 QPS、企业版 2000 QPS,超限返回 HTTP 429 并在响应头带 Retry-After。
连接池不要给每个租户独立开池,会直接把数据库连接打满。HikariCP 的经验公式是 poolSize = CPU核数 × 2 + 有效磁盘数,8 核机器配 20 左右,多租户共用这一个池。
数据量上来后按 tenant_id 哈希分库,比如固定 64 个分片,shard = tenant_id % 64,保证同一租户的数据落在同一库,避免跨库事务。监控指标也要按 tenant_id 打标签,把 P99 延迟和错误率做到租户粒度,才能定位「是哪个客户在拖慢系统」。
租户的开通、迁移、注销怎么做?
结论:租户生命周期必须做成一等公民的运维流程,不能靠人工跑 SQL。
注销走「软删除标记 + 30 天冷静期 + 异步物理清除」三步,独立库租户直接 DROP DATABASE,共享表租户按 tenant_id 分批删除,每批 5000 行避免长事务锁表。
从共享库升级到独立库的迁移流程是:双写 → 增量同步 → 校验行数与 checksum 一致 → 切读 → 切写 → 下线旧数据,正式切换前至少完整演练 2 次。线上 DDL 用 Flyway 或 Liquibase 管理,共享表只需执行一次,独立 schema 需要遍历所有租户逐个执行并记录执行结果。
把以上几件事串起来看,多租户系统的设计主线就是:先定隔离级别,再用数据库层强制 tenant_id 兜底安全,用上下文透传保证链路一致,用租户维度的限流分片保证公平,最后把租户生命周期流程化。这五点做到位,系统从 100 个租户扩到 10 万个租户,架构基本不用推倒重来。
原文链接:https://www.gj0.com/thread-340.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。