20 KiB
20 KiB
多租户设计
**本文引用的文件** - [OrgContext.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgContext.java) - [JwtAuthFilter.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/JwtAuthFilter.java) - [OrgRoutingDataSource.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgRoutingDataSource.java) - [OrgDataSourceManager.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceManager.java) - [OrgDataSourceFactory.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDataSourceFactory.java) - [OrgDatabaseConfigLoader.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfigLoader.java) - [OrgDatabaseConfig.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/database/OrgDatabaseConfig.java)目录
简介
本文件面向FMS系统基于orgId的多租户数据隔离设计,系统性阐述以下要点:
- 数据源路由:通过动态数据源切换实现按租户选择数据库连接。
- 上下文管理:使用线程安全的上下文对象在请求生命周期内传递租户与用户信息。
- 查询过滤机制:结合上下文与ORM/SQL层策略,确保所有数据访问均受租户边界约束。
- 权限与安全:在认证过滤器中完成鉴权、会话校验与上下文注入。
- 配置管理:以文件化方式加载各租户数据库连接参数,支持规范化与校验。
- 运维保障:涵盖连接池健康检查、资源清理、监控告警建议及备份恢复思路。
- 开发指南:给出在多租户环境下编写业务代码的最佳实践。
项目结构
围绕多租户能力,后端关键模块集中在数据库与配置层:
- 上下文与拦截器:负责在请求链路中设置和清理租户上下文。
- 数据源路由与管理:根据当前租户上下文动态选择并复用对应数据库连接池。
- 配置加载与工厂:从文件系统读取租户配置,创建并初始化Druid连接池。
graph TB
subgraph "请求处理"
F["JwtAuthFilter<br/>解析Token并设置上下文"]
end
subgraph "上下文"
Ctx["OrgContext<br/>ThreadLocal存储orgId/userId"]
end
subgraph "数据源路由"
RDS["OrgRoutingDataSource<br/>继承AbstractDataSource"]
Mgr["OrgDataSourceManager<br/>ConcurrentMap缓存数据源"]
Fac["OrgDataSourceFactory<br/>创建并初始化DruidDataSource"]
Ldr["OrgDatabaseConfigLoader<br/>加载并校验租户配置"]
Cfg["OrgDatabaseConfig<br/>记录url/username/password/driver"]
end
F --> Ctx
RDS --> Mgr
Mgr --> Ldr
Mgr --> Fac
Ldr --> Cfg
图表来源
- JwtAuthFilter.java:19-88
- OrgContext.java:3-53
- OrgRoutingDataSource.java:11-27
- OrgDataSourceManager.java:13-60
- OrgDataSourceFactory.java:9-51
- OrgDatabaseConfigLoader.java:19-65
- OrgDatabaseConfig.java:1-9
章节来源
- JwtAuthFilter.java:19-88
- OrgContext.java:3-53
- OrgRoutingDataSource.java:11-27
- OrgDataSourceManager.java:13-60
- OrgDataSourceFactory.java:9-51
- OrgDatabaseConfigLoader.java:19-65
- OrgDatabaseConfig.java:1-9
核心组件
- OrgContext:提供线程安全的orgId与userId存取,保证同一请求线程内的上下文一致性,并提供require系列方法用于强校验。
- JwtAuthFilter:在请求入口处解析JWT,校验会话有效性后,将orgId与userId写入上下文,并在finally中清理,避免上下文泄漏。
- OrgRoutingDataSource:作为主数据源入口,依据当前上下文或显式传入的orgId获取目标数据源连接。
- OrgDataSourceManager:维护orgId到DruidDataSource的映射,懒加载、缓存与关闭;对外暴露getConnection重载。
- OrgDataSourceFactory:封装DruidDataSource创建与连接池参数配置,包含空闲连接探测与NAT友好策略。
- OrgDatabaseConfigLoader:从指定目录加载{orgId}.properties,校验URL协议、必填字段与驱动类名,返回不可变配置对象。
- OrgDatabaseConfig:承载单个租户的连接元数据(url、用户名、密码、驱动)。
章节来源
- OrgContext.java:3-53
- JwtAuthFilter.java:19-88
- OrgRoutingDataSource.java:11-27
- OrgDataSourceManager.java:13-60
- OrgDataSourceFactory.java:9-51
- OrgDatabaseConfigLoader.java:19-65
- OrgDatabaseConfig.java:1-9
架构总览
下图展示了从HTTP请求进入、鉴权、上下文设置到数据源路由的完整流程。
sequenceDiagram
participant Client as "客户端"
participant Filter as "JwtAuthFilter"
participant Ctx as "OrgContext"
participant RDS as "OrgRoutingDataSource"
participant Mgr as "OrgDataSourceManager"
participant Ldr as "OrgDatabaseConfigLoader"
participant Fac as "OrgDataSourceFactory"
participant DS as "DruidDataSource(租户)"
Client->>Filter : "HTTP请求(携带Authorization)"
Filter->>Filter : "解析Token并校验会话"
Filter->>Ctx : "setOrgId()/setUserId()"
Filter-->>Client : "继续后续处理"
Note over Filter,Ctx : "业务调用前上下文已就绪"
RDS->>Mgr : "getConnection() 或 getConnection(orgId)"
alt orgId为空
Mgr->>Ctx : "requireOrgId()"
else orgId非空
Mgr->>Mgr : "normalizeOrgId(orgId)"
end
Mgr->>Ldr : "load(orgId)"
Ldr-->>Mgr : "OrgDatabaseConfig"
Mgr->>Fac : "create(orgId, config)"
Fac-->>Mgr : "DruidDataSource"
Mgr-->>RDS : "Connection"
RDS-->>Client : "返回结果"
Filter->>Ctx : "clear() (finally)"
图表来源
- JwtAuthFilter.java:40-88
- OrgRoutingDataSource.java:18-27
- OrgDataSourceManager.java:24-35
- OrgDatabaseConfigLoader.java:36-65
- OrgDataSourceFactory.java:15-51
详细组件分析
上下文与线程安全:OrgContext
- 使用ThreadLocal分别保存orgId与userId,保证同一线程内上下文一致且互不干扰。
- 提供require系列方法,在未设置时抛出异常,便于快速定位遗漏设置的场景。
- 提供clear方法,配合过滤器在finally中清理,防止上下文泄漏。
classDiagram
class OrgContext {
+setOrgId(orgId)
+getOrgId() String
+requireOrgId() String
+setUserId(userId)
+getUserId() String
+requireUserId() String
+clear()
}
图表来源
章节来源
认证与上下文注入:JwtAuthFilter
- 跳过OPTIONS与登录路径,其他请求需携带Bearer Token。
- 校验Token与活跃会话,失败则返回统一错误响应。
- 成功时将orgId与userId写入上下文,并在finally中清理。
flowchart TD
Start(["进入过滤器"]) --> CheckMethod{"是否OPTIONS或登录?"}
CheckMethod --> |是| Next["放行"]
CheckMethod --> |否| Parse["解析Token并校验会话"]
Parse --> Valid{"校验通过?"}
Valid --> |否| Deny["返回未授权/会话被顶替"]
Valid --> |是| SetCtx["设置orgId/userId到上下文"]
SetCtx --> DoFilter["执行后续过滤器/处理器"]
DoFilter --> Finally["finally中清理上下文"]
Finally --> End(["结束"])
图表来源
章节来源
动态数据源路由:OrgRoutingDataSource
- 作为Primary数据源,委托给OrgDataSourceManager获取连接。
- 无参getConnection默认从上下文取orgId;有参getConnection直接使用传入orgId。
classDiagram
class OrgRoutingDataSource {
-dataSourceManager : OrgDataSourceManager
+getConnection() Connection
+getConnection(username,password) Connection
}
class OrgDataSourceManager
OrgRoutingDataSource --> OrgDataSourceManager : "委托获取连接"
图表来源
章节来源
数据源管理与缓存:OrgDataSourceManager
- 使用ConcurrentMap缓存orgId到DruidDataSource的映射,避免重复创建。
- 懒加载:首次访问某租户时加载配置并创建连接池。
- 提供关闭单个与全部数据源的能力,应用销毁时自动释放资源。
flowchart TD
GetConn["getConnection(orgId?)"] --> Normalize["规范化orgId"]
Normalize --> CacheHit{"缓存命中?"}
CacheHit --> |是| ReturnDS["返回已有DruidDataSource"]
CacheHit --> |否| LoadCfg["加载租户配置"]
LoadCfg --> CreateDS["创建并初始化DruidDataSource"]
CreateDS --> PutCache["放入缓存"]
PutCache --> ReturnDS
ReturnDS --> Conn["getConnection()"]
图表来源
章节来源
配置加载与校验:OrgDatabaseConfigLoader
- 从配置目录读取{orgId}.properties,严格校验URL必须为SQL Server协议。
- 规范化orgId(去空白、大写、格式校验),防止路径穿越与非法字符。
- 返回不可变的OrgDatabaseConfig,包含url、username、password、driver。
flowchart TD
Start(["load(orgId)"]) --> Norm["normalizeOrgId(orgId)"]
Norm --> Path["拼接配置文件路径并校验"]
Path --> Read["读取properties"]
Read --> Validate{"校验url/username/password"}
Validate --> |通过| Build["构建OrgDatabaseConfig"]
Validate --> |失败| Error["抛出异常"]
Build --> End(["返回配置"])
Error --> End
图表来源
章节来源
连接池创建与健康:OrgDataSourceFactory
- 基于DruidDataSource创建连接池,名称与orgId绑定,便于监控区分。
- 启用KeepAlive与定期ping,对抗云环境NAT/防火墙回收空闲TCP会话。
- 初始化失败时关闭资源并抛出明确异常,便于快速定位问题。
classDiagram
class OrgDataSourceFactory {
-properties : OrgDatabaseProperties
+create(orgId, config) DruidDataSource
}
class DruidDataSource
OrgDataSourceFactory --> DruidDataSource : "创建并初始化"
图表来源
章节来源
依赖关系分析
- JwtAuthFilter依赖OrgContext进行上下文注入,并依赖会话注册表进行会话校验。
- OrgRoutingDataSource依赖OrgDataSourceManager,后者依赖配置加载器与工厂。
- OrgDataSourceManager内部维护ConcurrentMap,保证并发安全与高性能查找。
- OrgDatabaseConfigLoader对文件系统IO与正则校验,承担安全边界职责。
graph LR
JWT["JwtAuthFilter"] --> CTX["OrgContext"]
RDS["OrgRoutingDataSource"] --> MGR["OrgDataSourceManager"]
MGR --> LDR["OrgDatabaseConfigLoader"]
MGR --> FAC["OrgDataSourceFactory"]
LDR --> CFG["OrgDatabaseConfig"]
图表来源
- JwtAuthFilter.java:19-88
- OrgRoutingDataSource.java:11-27
- OrgDataSourceManager.java:13-60
- OrgDatabaseConfigLoader.java:19-65
- OrgDataSourceFactory.java:9-51
- OrgDatabaseConfig.java:1-9
章节来源
- OrgRoutingDataSource.java:11-27
- OrgDataSourceManager.java:13-60
- OrgDatabaseConfigLoader.java:19-65
- OrgDataSourceFactory.java:9-51
- JwtAuthFilter.java:19-88
性能考虑
- 连接池复用:OrgDataSourceManager使用ConcurrentMap缓存数据源,避免重复创建带来的开销。
- 懒加载:仅在首次访问租户时加载配置并创建连接池,降低冷启动成本。
- 连接健康:启用KeepAlive与定期ping,减少因云环境NAT/防火墙导致的连接失效。
- 线程安全:ThreadLocal与ConcurrentMap保证高并发下的正确性与低锁竞争。
- 建议扩展:
- 为每个租户配置合理的初始大小、最大活动数与等待超时,避免连接耗尽。
- 引入慢查询日志与SQL审计,识别热点租户与瓶颈语句。
- 对高频访问的租户可预热连接池,缩短首请求延迟。
[本节为通用性能建议,不直接分析具体文件]
故障排查指南
- 未设置上下文:当业务代码调用requireOrgId/requireUserId但未设置时,会抛出异常。请确认JwtAuthFilter是否正确执行,以及请求是否携带有效Token。
- 租户配置缺失或非法:加载器会抛出异常,提示缺少必要字段或URL协议不符。请检查对应{orgId}.properties是否存在且内容合法。
- 连接池初始化失败:工厂会在初始化失败时关闭资源并抛出异常。请核对驱动类名、URL、用户名与密码,以及网络连通性。
- 会话失效或被顶替:过滤器会返回统一错误码与消息。请检查会话注册表状态与Token有效期。
- 资源未释放:应用停止时应确保所有数据源被关闭。管理器已在销毁钩子中关闭全部连接池。
章节来源
- OrgContext.java:24-53
- JwtAuthFilter.java:40-88
- OrgDatabaseConfigLoader.java:36-65
- OrgDataSourceFactory.java:44-51
- OrgDataSourceManager.java:51-55
结论
本方案通过“上下文+路由+配置加载”的组合,实现了基于orgId的多租户数据隔离:
- 请求级上下文确保租户与用户信息贯穿整个调用链。
- 动态数据源路由将不同租户的数据访问隔离到各自数据库。
- 配置加载与连接池管理提供了可扩展、可观测、易运维的基础设施。 在此基础上,结合ORM层的查询过滤与权限控制,可进一步实现行级与列级的细粒度隔离。
[本节为总结性内容,不直接分析具体文件]
附录:开发指南与最佳实践
- 始终通过上下文访问租户信息:业务代码应使用requireOrgId/requireUserId获取当前租户与用户,避免硬编码。
- 保证上下文清理:若自定义过滤器或异步任务,需在finally中清理上下文,或使用框架提供的上下文传播机制。
- 查询强制加租户条件:在DAO/ORM层对所有SQL追加orgId过滤条件,防止越权访问。
- 幂等与重试:对跨租户的网络与DB操作增加重试与熔断策略,提升稳定性。
- 配置管理:
- 每个租户一个{orgId}.properties,严格校验URL与凭据。
- 敏感信息建议使用加密或密钥管理服务,避免明文落盘。
- 备份与恢复:
- 按租户维度制定备份策略(全量/增量),保留足够周期。
- 恢复时需先验证配置与连接,再执行导入,并校验数据一致性。
- 监控与告警:
- 暴露连接池指标(活跃、空闲、等待、错误率)与慢查询统计。
- 对配置加载失败、连接池初始化失败、会话失效等事件设置告警。
- 测试建议:
- 针对多租户场景准备隔离的测试库与配置,覆盖正常与异常分支。
- 模拟上下文缺失、配置非法、连接失败等场景,验证错误路径。
[本节为通用指导,不直接分析具体文件]