核心是将tenant_id作为数据结构一等公民,强制参与主键、外键、索引及分区,通过结构化容器、租户感知查询器和物理存储对齐实现天然租户隔离。

核心是让数据访问天然携带租户身份,不靠人工拼条件,而是靠结构设计把租户信息“焊”进查询路径里。
租户标识必须成为数据结构的一等公民
在实体类、DTO、数据库表结构中,tenant_id 不是可选字段,而是主键级约束字段。它要参与主键组合(如 (tenant_id, id))、作为外键强制引用、在索引中前置(如联合索引 (tenant_id, created_at))。这样,任何基于该结构的查询天然受限于租户边界。
- 建表时 tenant_id 必须设为 NOT NULL + INDEX,禁止空值或默认值
- ORM 实体类中标记 @TenantId 或使用抽象基类统一继承,避免漏加
- API 入参 DTO 自动校验 tenant_id 是否与当前上下文一致,不一致直接 403
用结构化容器替代扁平数据传递
避免在服务调用链中只传原始 ID 或 Map,改用带租户上下文的结构体封装数据。例如定义 TenantData
- 流程启动时生成 TenantData
,后续审批节点、通知服务、文件上传回调都复用该结构 - 消息队列投递时序列化 TenantData,消费者反序列化后无需再查上下文
- 缓存 key 设计为 tenantId:entityType:id,杜绝跨租户缓存污染
查询构造器内建租户感知能力
不依赖手写 SQL 或动态拼 WHERE,而是把租户逻辑下沉到数据访问层的结构化接口中。例如提供 QueryBuilder.forTenant(tenantId).selectFrom(“orders”).where(…),该 builder 内部自动注入 tenant_id = ? 条件,并校验所有 where 字段是否属于租户视图范围。
- 禁止开放 rawQuery(String sql, Object... args) 这类无租户防护的底层方法
- MyBatis 使用
<sql></sql>片段封装通用租户过滤模板,所有 mapper 引用它 - JPA Repository 接口方法名强制包含 ByTenant(如 findAllByTenantIdAndStatus)
索引与分区按租户结构对齐
物理存储结构要呼应逻辑隔离意图。在支持分区的数据库(如 YashanDB、PostgreSQL)中,按 tenant_id 做 LIST 或 HASH 分区;在 MySQL 中,至少为 tenant_id 建立前导复合索引。让数据库执行计划天然走租户剪枝,而不是靠应用层过滤后再丢弃大量行。
- 分区键必须是 tenant_id 或其哈希值,确保单租户查询只扫描一个分区
- 所有高频查询字段都与 tenant_id 组成联合索引,避免回表和全扫
- 定期分析租户数据分布,对倾斜租户单独优化(如拆分大租户为子租户)











