|
|
@@ -0,0 +1,226 @@
|
|
|
+# SaaS 多租户架构方案
|
|
|
+
|
|
|
+> 状态:**方案草案,待后续开工评审**
|
|
|
+> 适用范围:面向中小型工厂的 SaaS 系统(大型工厂走本地部署,不进 SaaS)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 背景与前提
|
|
|
+
|
|
|
+- 当前系统为 SaaS 模式,需要同时接入多家不同工厂。
|
|
|
+- 目标客户均为**中小型工厂**,每家 2–3 年的数据总量不会大于 40G。
|
|
|
+- **大型工厂统一走本地部署**,不进入 SaaS 平台。
|
|
|
+- 现有数据库中租户标识字段**不统一**:部分表使用 `domain`,部分表使用 `factory_id`。
|
|
|
+- 其中 `domain` 与 `factory_id` 当前为 **1:1** 关系。
|
|
|
+
|
|
|
+## 2. 目标
|
|
|
+
|
|
|
+- 建立清晰、统一的多租户数据隔离模型。
|
|
|
+- 不引入过度复杂度(不做分库分表、不做一租户一库)。
|
|
|
+- 对后续演进保留清晰的扩展点(独立库、大表归档、报表只读等)。
|
|
|
+- 为历史表中 `domain` / `factory_id` 字段不统一的问题,给出统一的租户标识规范。
|
|
|
+
|
|
|
+## 3. 最终架构选型
|
|
|
+
|
|
|
+采用 **共享实例 + 共享库 + 共享表 + `tenant_id` 逻辑隔离**。
|
|
|
+
|
|
|
+不采用:
|
|
|
+
|
|
|
+- 分库分表
|
|
|
+- 一工厂一库
|
|
|
+- 一工厂一套独立部署
|
|
|
+- 引入分布式数据库中间件
|
|
|
+
|
|
|
+### 3.1 选型理由
|
|
|
+
|
|
|
+- 目标客户体量小、数据量小,共享库多租户性价比最高。
|
|
|
+- 多租户 SaaS 的核心问题是**租户隔离、权限、配置管理**,分库分表主要解决的是超大规模吞吐,与当前场景不匹配。
|
|
|
+- 统一架构便于统一升级、统一运维、统一报表。
|
|
|
+- 大型工厂已走本地部署,不需要为了极端体量客户提前把 SaaS 做重。
|
|
|
+
|
|
|
+### 3.2 架构总览
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ User[FactoryUser]
|
|
|
+ Web[WebOrApp]
|
|
|
+ Gateway[GatewayOrNginx]
|
|
|
+ App[SaaSApp]
|
|
|
+ TenantCtx[TenantContext]
|
|
|
+ SharedDb[(SharedBizDb)]
|
|
|
+ HotTables[(HotDetailTables)]
|
|
|
+ SummaryTables[(SummaryTables)]
|
|
|
+ ArchiveDb[(ArchiveOrHistoryDb)]
|
|
|
+ Cache[RedisCache]
|
|
|
+ FileStore[ObjectStorage]
|
|
|
+
|
|
|
+ User --> Web
|
|
|
+ Web --> Gateway
|
|
|
+ Gateway --> App
|
|
|
+ App --> TenantCtx
|
|
|
+ TenantCtx --> SharedDb
|
|
|
+ SharedDb --> HotTables
|
|
|
+ HotTables --> ArchiveDb
|
|
|
+ App --> SummaryTables
|
|
|
+ App --> Cache
|
|
|
+ App --> FileStore
|
|
|
+```
|
|
|
+
|
|
|
+## 4. 租户标识统一规范
|
|
|
+
|
|
|
+### 4.1 字段统一
|
|
|
+
|
|
|
+| 用途 | 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| 系统内部租户主键 | `tenant_id` | `bigint` | 所有业务表隔离、ORM 过滤、权限、缓存 Key、日志统一使用 |
|
|
|
+| 业务可读编码 | `tenant_code` | `varchar(64)` | 对外/对内可读编码,允许变更 |
|
|
|
+| 显示名称 | `tenant_name` | `varchar(128)` | 仅展示 |
|
|
|
+| 访问入口 | `domain` | `varchar(128)` | 仅用于登录入口解析,不作为关联键 |
|
|
|
+| 工厂业务主键 | `factory_id` | `bigint` | 仅属于 `factory` 表本身,不再承担租户隔离职责 |
|
|
|
+
|
|
|
+### 4.2 语义边界
|
|
|
+
|
|
|
+- `tenant_id`:**系统维度**。数据隔离、ORM 过滤、权限、缓存、日志、文件路径、消息主题统一以此为准。
|
|
|
+- `domain`:**入口维度**。只用于登录时解析成 `tenant_id`,不参与业务表关联。
|
|
|
+- `factory_id`:**业务维度**。只属于 `factory` 业务实体,不再兼任租户标识。
|
|
|
+
|
|
|
+### 4.3 为什么不继续用 `domain` 或 `factory_id` 作为租户键
|
|
|
+
|
|
|
+- `domain` 属于可变业务属性,客户可能更换域名或绑定多个域名,作为关联键稳定性差。
|
|
|
+- `factory_id` 语义偏业务,将来若出现「集团-工厂」或「一租户多工厂」扩展,命名会非常尴尬。
|
|
|
+- `tenant_id` 语义纯粹,与业务实体解耦,后续演进最稳。
|
|
|
+
|
|
|
+## 5. 设计要点
|
|
|
+
|
|
|
+### 5.1 表结构
|
|
|
+
|
|
|
+- 所有核心业务表统一增加:
|
|
|
+ - `tenant_id`
|
|
|
+ - `created_time` / `updated_time`
|
|
|
+ - `created_by` / `updated_by`
|
|
|
+- 唯一键升级为复合唯一:`tenant_id + 业务键`(例如 `tenant_id + code`)。
|
|
|
+- 常用索引以 `tenant_id` 打头,例如:
|
|
|
+ - `tenant_id + status`
|
|
|
+ - `tenant_id + created_time`
|
|
|
+ - `tenant_id + business_key`
|
|
|
+
|
|
|
+### 5.2 应用层
|
|
|
+
|
|
|
+- 登录后写入 `TenantContext`。
|
|
|
+- ORM/仓储层**统一自动注入 `tenant_id` 过滤**,防止漏写。
|
|
|
+- 写入操作自动带 `tenant_id`。
|
|
|
+- 跨租户后台(超管/运营)走独立数据访问入口,显式放行。
|
|
|
+
|
|
|
+### 5.3 外围资源
|
|
|
+
|
|
|
+- 对象存储路径前缀带租户维度。
|
|
|
+- 缓存 Key 命名带租户维度。
|
|
|
+- 消息队列主题/事件总线按租户隔离。
|
|
|
+- 操作日志、审计日志记录 `tenant_id`。
|
|
|
+
|
|
|
+## 6. 明细/日志表策略
|
|
|
+
|
|
|
+采用 **共享表起步 + 预留归档能力(archive-ready)**。适用于设备采集、日志、履历、追溯流水等高频写入表。
|
|
|
+
|
|
|
+- 初期仍放共享表,不分表、不分库。
|
|
|
+- 建表时预留:
|
|
|
+ - `tenant_id + created_time` 索引,支持时间范围查询。
|
|
|
+ - 按月/季度归档到历史表的能力。
|
|
|
+ - 冷数据可搬迁到归档库。
|
|
|
+- 只在单表确实出现性能或容量压力时,对**该表**单独启动归档,不动整体架构。
|
|
|
+
|
|
|
+## 7. 报表与统计策略
|
|
|
+
|
|
|
+采用 **汇总表 + 定时聚合**。
|
|
|
+
|
|
|
+- 不直接在交易表上做复杂统计。
|
|
|
+- 通过定时任务或事件驱动,维护汇总表。
|
|
|
+- 汇总表同样带 `tenant_id`。
|
|
|
+- 若未来数据量真正上来,再考虑引入只读副本或分析库。
|
|
|
+
|
|
|
+## 8. 迁移思路(基于 domain : factory_id = 1:1)
|
|
|
+
|
|
|
+> 当前仅为迁移**思路**记录,不包含实施步骤。实施前需单独评审。
|
|
|
+
|
|
|
+### 8.1 新建租户主表
|
|
|
+
|
|
|
+示意结构:
|
|
|
+
|
|
|
+```text
|
|
|
+sys_tenant
|
|
|
+- tenant_id bigint 主键
|
|
|
+- tenant_code varchar(64) 唯一
|
|
|
+- tenant_name varchar(128)
|
|
|
+- domain varchar(128) 唯一
|
|
|
+- factory_id_legacy bigint 过渡期兼容用
|
|
|
+- status tinyint
|
|
|
+- created_time / updated_time
|
|
|
+```
|
|
|
+
|
|
|
+由于当前 `domain` 与 `factory_id` 是 1:1,可以一次性建立 `tenant_id` 与旧 `domain`、`factory_id` 的映射。
|
|
|
+
|
|
|
+### 8.2 业务表 `tenant_id` 补齐
|
|
|
+
|
|
|
+- 所有核心业务表新增 `tenant_id` 列。
|
|
|
+- 写入层改为双写:同时填 `tenant_id` 与旧字段。
|
|
|
+- 读取层逐步切到 `tenant_id`。
|
|
|
+- ORM/仓储层统一改为基于 `tenant_id` 过滤。
|
|
|
+
|
|
|
+### 8.3 旧字段处置
|
|
|
+
|
|
|
+- `domain` 最终只保留在 `sys_tenant`,其他表不再冗余。
|
|
|
+- `factory_id` 仅保留在 `factory` 表本身的业务语义下,不再承担租户隔离职责。
|
|
|
+- 过渡期内,业务表中的旧字段可继续存在,仅作历史兼容,不用于关联。
|
|
|
+
|
|
|
+### 8.4 登录与入口
|
|
|
+
|
|
|
+- 用户通过 `domain` 访问。
|
|
|
+- 登录时将 `domain` 解析为 `tenant_id`,写入 `TenantContext`。
|
|
|
+- 后续所有查询均以 `tenant_id` 过滤。
|
|
|
+
|
|
|
+## 9. 未来演进预留
|
|
|
+
|
|
|
+仅作设计预留,当前不启用:
|
|
|
+
|
|
|
+- 某租户数据量远超预期时:支持将其**切到独立库**,通过租户→数据源路由实现。
|
|
|
+- 明细表压力上升时:对该表单独做**按时间归档或历史表拆分**。
|
|
|
+- 报表压力上升时:引入**只读副本或分析库**。
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ Start[Start]
|
|
|
+ SingleDb[SingleDbMultiTenant]
|
|
|
+ Archive[ArchiveHotTablesByTime]
|
|
|
+ Summary[SummaryTables]
|
|
|
+ LocalDeploy[LargeFactoryLocalDeployment]
|
|
|
+ FutureTenantDb[OptionalTenantIsolatedDb]
|
|
|
+
|
|
|
+ Start --> SingleDb
|
|
|
+ SingleDb --> Archive
|
|
|
+ SingleDb --> Summary
|
|
|
+ SingleDb --> LocalDeploy
|
|
|
+ SingleDb --> FutureTenantDb
|
|
|
+```
|
|
|
+
|
|
|
+## 10. 风险与注意事项
|
|
|
+
|
|
|
+- **漏加 `tenant_id` 过滤**是最大风险,必须在 ORM/仓储层做统一拦截,不允许业务层自由决定。
|
|
|
+- **唯一键改造**要覆盖到所有历史业务唯一索引,避免不同工厂编码冲突或串数据。
|
|
|
+- **迁移期双写**要考虑幂等与补偿,避免新旧字段不一致。
|
|
|
+- **后台/超管/跨租户接口**必须单独显式放行,不能误走统一过滤入口。
|
|
|
+
|
|
|
+## 11. 暂不做的事
|
|
|
+
|
|
|
+- 不做分库分表。
|
|
|
+- 不做一租户一库。
|
|
|
+- 不做一租户一套独立部署。
|
|
|
+- 不引入分布式数据库中间件。
|
|
|
+- 不提前为“物理隔离客户”搭建路由体系,仅保留能力预留。
|
|
|
+
|
|
|
+## 12. 后续动作(未排期)
|
|
|
+
|
|
|
+- 方案评审通过后,再单独拉出「实施计划」文档,细化到:
|
|
|
+ - 表改造清单与顺序
|
|
|
+ - ORM/仓储层租户过滤改造点
|
|
|
+ - 数据迁移与双写方案
|
|
|
+ - 灰度与回滚方案
|