不值得,绝大多数场景下这是反模式,会把运维、备份、聚合和权限控制全拖进泥潭;仅gridfs和极端合规需求才需独立集合,其余应默认用共享集合+tenantid字段兜底。

不值得,绝大多数场景下这是反模式,会把运维、备份、聚合和权限控制全拖进泥潭。
为什么独立集合名(如orders_tenant_123)不是真隔离
它既没达到独立数据库的物理隔离效果,又丧失了共享集合的可维护性。MongoDB 的 namespace 元数据开销随集合数线性增长,分片集群中 config.collections 文档膨胀后,mongos 路由元数据加载变慢,甚至影响整个集群响应。
- 租户删库容易,删集合难:误删
orders_tenant_123后无法用collMod恢复结构,只能从备份里捞,而独立数据库可直接dropDatabase并重建 -
$lookup失效:跨租户统计或关联查询必须退回到应用层做 N+1 合并,没法写聚合管道 - 索引冗余爆炸:每个租户集合都要单独建
{ status: 1, createdAt: -1 }这类索引,内存占用翻倍,缓存效率反而下降
真正需要独立集合的两个例外场景
只有当这两个条件同时满足时,才考虑按租户拆集合:
- 租户数据量级差异极大 —— 比如一个头部客户占总订单量的 80%,其他 99 个租户加起来才 20%
- 合规要求磁盘级硬隔离(例如 GDPR 明确禁止同一块 SSD 上混存多租户 PII 数据),且你已放弃 MongoDB,改用支持透明存储加密(TDE)+ Schema 级租户的国产数据库(如金仓)
注意:仅靠“想隔离”或“怕出 bug”不算合理动因。
共享集合 + tenantId 字段才是默认选项
它不是妥协,而是权衡后的最优解。关键不在“是否共享”,而在“如何兜底”:
- 所有查询必须显式带
{ tenantId: "xxx" },中间件注入不可信 —— 攻击者绕过 API 层直连 mongosh 就能跳过中间件 -
tenantId必须是复合索引前导字段,比如{ tenantId: 1, status: 1, updatedAt: -1 };单字段索引在 tenantId 基数低时(如只有 20 个大客户)极易触发全表扫描 - Mongoose 层加
pre('find')钩子强制校验tenantId存在且非空,再配合 MongoDB 5.0+ 的$jsonSchema文档校验规则 - 唯一约束必须复合,比如
{ email: 1, tenantId: 1 },否则邮箱字段全局唯一会卡住多租户注册
GridFS 除外:这里独立 bucketName 是刚需
GridFS 的 files 和 chunks 是普通集合,没有行级权限能力。靠 metadata.tenantId 过滤等同于裸奔 —— 一旦漏条件或被直连,所有租户文件一览无余。
- 必须动态构造
GridFSBucket实例,bucketName设为tenant_${tenantId}形式 -
tenantId要白名单校验(只允许字母、数字、下划线),防止注入非法字符导致集合名无效 - 权限必须落到具体集合:给租户账号授予
tenant_acme_001.files和tenant_acme_001.chunks的读写权,禁用readWrite数据库角色
这个例外恰恰说明:集合级隔离是 MongoDB 唯一能提供“硬隔离”的粒度,但代价太高,所以只该用在 GridFS 这种元数据无法可靠过滤的场景。











