为多租户环境设计数据库架构需要仔细考虑数据隔离、可扩展性和维护开销。实体关系图(ERD)作为这些决策的蓝图,规定了数据在租户间的结构方式。选择合适的方案会影响性能、安全性以及系统随时间演进的能力。本指南将探讨主要的架构模式、它们对ERD的影响,以及每种策略所涉及的权衡。

🔍 理解数据建模中的多租户
多租户允许单个软件实例服务于多个客户,这些客户通常被称为租户。在数据库设计的背景下,核心挑战在于如何在保持效率的同时决定如何分离租户数据。ERD必须清晰地反映这些分离边界。
- 租户:使用系统的单个客户或组织。
- 共享系统:应用程序逻辑以及潜在的底层基础设施。
- 数据隔离:确保一个租户无法访问另一个租户的数据。
设计选择主要围绕隔离边界的位置展开。它存在于数据库级别、模式级别还是行级别?每种选择都需要特定的ERD结构。
🏗️ 策略一:每个租户一个数据库
在此模型中,每个租户都获得一个专用的数据库实例。这提供了最高级别的隔离性和安全性。从ERD角度来看,所有数据库的模式完全相同,但物理分离是绝对的。
📊 ERD结构
单个租户数据库的ERD图与标准的单租户设计完全相同。不需要一个“租户ID"“列,因为数据库边界本身即充当过滤器。
- 表结构:表仅包含与特定租户相关的数据。
- 外键:应用标准引用完整性,无需感知租户。
- 索引:针对该租户的特定数据量进行优化。
✅ 优势
- 完全隔离:一个数据库的漏洞不会影响其他数据库。
- 定制化:模式修改可以应用于特定租户,而不影响其他租户。
- 性能:同一连接池或磁盘I/O上无其他租户的争用。
❌ 缺点
- 成本:由于存在多个实例,基础设施成本较高。
- 维护:架构更新需要部署到每个数据库实例。
- 复杂性:在大规模下,管理连接和编排变得困难。
🏗️ 策略 2:每个租户一个架构
该方法介于前两种方案之间。每个租户在同一个数据库服务器中获得一个专用架构。这减少了管理多个数据库连接的开销,同时保持了逻辑隔离。
📊 ERD 结构
ERD 与单租户模型保持一致,但命名空间发生了变化。表存在于特定的架构命名空间中,而不是公共命名空间。
- 表名:标准命名约定(例如:”
users,orders). - 架构名:唯一标识符(例如:”
schema_tenant_a,schema_tenant_b). - 连接性:应用程序连接到当前租户的特定架构。
✅ 优点
- 隔离性:比共享架构模型具有更强的隔离性。
- 管理:比独立的数据库实例更容易管理。
- 备份:可以独立地还原或备份各个模式。
❌ 缺点
- 资源使用情况:仍然比完全共享模型消耗更多资源。
- 查询复杂性:跨租户聚合数据需要动态切换模式。
- 模式漂移:在多个租户之间保持模式同步非常耗费人力。
🏗️ 策略 3:共享数据库,共享模式
这是 SaaS 应用中最常见的方案。所有租户共享同一个数据库和相同的表。数据分离通过一个唯一标识符列在逻辑上实现。
📊 ERD 结构
ERD 必须明确包含一个“tenant_id“列,用于存储租户特定数据的每张表中。该列作为分区键。
- 核心表:
users,orders,products都包含一个“tenant_id. - 共享表:共享表:
roles“或“permissions“等表可能会在所有租户之间共享。 - 约束条件:外键可能需要限定范围,以确保在租户上下文中维护引用完整性。
✅ 优势
- 成本效益:基础设施成本最低。
- 维护:架构变更会立即应用于所有租户。
- 分析:更容易聚合数据以进行系统范围的报告。
❌ 劣势
- 复杂查询:每个查询都需要按
租户 ID. - 性能:如果某个租户消耗过多资源,则存在较高的争用风险。
- 安全性:逻辑错误导致数据泄露的风险更高。
🏗️ 策略 4:混合模型
混合方法结合了上述策略的要素。例如,为标准数据使用共享架构,而为高级层级或特定高价值租户使用专用架构。
📊 ERD 结构
ERD 变得更加复杂,区分共享表和租户特定表。
- 全局表:存储配置或共享元数据。
- 租户表:存储带有
租户 ID的租户数据,或存储在单独的架构中。 - 关联:连接操作必须考虑数据的范围。
🛡️ 数据隔离与安全考量
无论选择何种策略,数据隔离都至关重要。实体关系图(ERD)必须支持能够防止意外数据访问的机制。
🔒 行级安全
在共享模式架构中,可以定义行级安全(RLS)策略。数据库引擎将限制对满足以下条件的行的访问:tenant_id与经过认证的上下文相匹配。
- 实现方式:策略会对每次
SELECT,UPDATE以及DELETE操作强制执行检查。 - 优势:防止应用程序级别的错误导致数据泄露。
- 对 ERD 的影响:需要在所有相关表中显式添加
tenant_id列。
🔒 外键约束
在共享模型中,确保跨租户的引用完整性可能较为复杂。理想情况下,外键不应指向跨越多个租户的表,除非该关系被明确定义为全局关系。
- 自引用:如果一张表引用自身(例如,
parent_id),tenant_id必须在两侧保持一致。 - 全局引用:如
类别可能是全局的,允许任何租户引用它们。
⚡ 性能与扩展策略
随着租户数量的增长,性能成为一个关键问题。实体关系图(ERD)设计直接影响系统的扩展能力。
📈 索引策略
索引对查询性能至关重要。在共享架构中,tenant_id列应作为主复合键的一部分或进行重度索引。
- 复合索引:
(tenant_id, created_at)可高效地按租户和时间进行过滤。 - 部分索引:索引可以仅针对特定条件创建,从而减小索引大小。
- 避免:对无助于租户过滤的列进行索引。
📦 分区
表分区有助于管理大型数据集。数据可按tenant_id或按租户内的时间范围进行分区。
- 范围分区:按日期范围拆分数据。
- 列表分区:按特定租户 ID 拆分数据。
- 管理:可以分离或归档分区以提升性能。
🔧 维护与架构演进
软件不断演进。需要添加表、修改列或更改类型。所选架构决定了这些变更所需的工作量。
🔄 架构更新
- 共享架构:单个迁移脚本即可更新所有租户的架构。这是最简单的路径。
- 每个租户一个数据库:迁移脚本必须针对每个数据库实例执行。需要实现自动化。
- 每个租户一个模式:类似于每个租户一个数据库,但在同一实例内进行管理。
📝 向后兼容性
修改实体关系图(ERD)时,请确保向后兼容性,以避免停机。
- 添加列:首先使用可为空的列,然后填充数据,最后将其设为非空。
- 删除列:在删除列之前先重命名,以防止破坏性变更。
- 版本控制:如果租户可以选择退出更新,请考虑对模式本身进行版本控制。
📋 架构方案比较
| 特性 | 每个租户一个数据库 | 每个租户一个模式 | 共享模式 |
|---|---|---|---|
| 隔离性 | 高 | 中 | 低 |
| 成本 | 高 | 中 | 低 |
| 维护 | 复杂 | 中 | 简单 |
| 查询性能 | 高(无过滤) | 中 | 可变(需要过滤) |
| ERD 复杂度 | 简单(按数据库) | 简单(按模式) | 复杂(需要 tenant_id) |
| 可扩展性 | 水平 | 垂直 | 垂直/水平 |
✅ 最佳实践检查清单
在最终确定多租户系统的 ERD 之前,请确保满足以下标准。
- 定义租户范围:明确识别哪些数据属于特定租户,哪些是全局数据。
- 标准化命名:在所有表中对
tenant_id列使用一致的命名约定。 - 实施约束:尽可能使用数据库约束来防止跨租户的数据访问。
- 规划租户流失:设计支持租户入驻和退订(数据删除或归档)。
- 测试隔离性:定期测试以确保一个租户无法查询另一个租户的数据。
- 记录关系:在 ERD 文档中清晰记录外键关系。
- 监控性能:为可能表明特定租户瓶颈的慢查询设置警报。
🧩 处理边界情况
现实场景往往会引入标准实体关系图(ERD)无法立即涵盖的复杂性。
🔄 租户合并
有时,两个租户会合并为一个。在共享模式架构下,这需要将行从一个tenant_id移动到另一个。在每租户一个数据库的模型中,这涉及合并两个完整的数据库。
- 数据一致性:确保合并过程中没有数据丢失。
- 去重:处理合并过程中可能产生的重复记录。
📉 租户流失
租户会离开。选择删除数据还是归档数据会影响实体关系图(ERD)。
- 软删除:添加一个
is_deleted标志以保留数据以满足合规要求。 - 硬删除:彻底删除行。确保级联删除配置正确,以避免产生孤立记录。
- 归档:将旧租户数据移至冷存储表,同时保持架构完整。
🔗 与应用逻辑集成
实体关系图(ERD)并非孤立存在,它必须与应用层无缝集成。
- 中间件:使用应用级中间件自动注入
tenant_id到每个查询中。 - ORM 配置:配置对象关系映射(ORM)工具以处理租户作用域。
- API 设计:确保 API 端点在返回数据前验证租户上下文。
🎯 关于设计的最终思考
为多租户环境选择适当的数据库设计,是在隔离性与效率之间寻求平衡。实体关系图(ERD)充当定义这些边界的契约。不存在单一的完美解决方案;选择取决于对安全性、成本和规模的具体要求。通过理解每种策略的影响,架构师可以构建出稳健、可扩展且安全的系统。
专注于清晰的数据建模实践,可确保随着租户数量的增长,系统仍易于维护。定期将实体关系图(ERD)与实际使用模式进行对比审查,有助于在瓶颈或安全漏洞演变为关键问题之前将其识别出来。
最终目标是设计出一个既能支持业务发展,又不损害数据完整性的方案。在实体关系图(ERD)阶段进行周密规划,可避免后期出现昂贵的重构工作。











