多租户或多环境下如何设计抽象工厂?
简化版
多租户或多环境下,抽象工厂通常由租户、环境、region、厂商、版本等配置共同决定。设计重点是把工厂选择放在清晰的上下文边界内,缓存 key 覆盖所有影响因素,并保证同一租户或同一次请求使用同一产品族,避免配置串用和资源泄漏。
详细版
在多租户 SaaS、混合云、测试/预发/生产多环境中,抽象工厂不能只按一个 type 选择。不同租户可能使用不同云厂商、不同凭证、不同 region;不同环境可能使用 mock、沙箱或生产实现。
一个较稳的设计会包含:
FactoryKey:描述租户、环境、厂商、region、凭证版本;FactoryProvider:根据 key 获取或创建具体工厂;- 工厂缓存:避免重复初始化昂贵资源;
- 生命周期管理:配置更新后关闭旧工厂;
- 启动和运行时校验:防止产品族和配置不一致。
不要让每个产品分别读取租户配置,否则同一请求可能拿到不同环境或不同厂商的产品。入口处确定工厂,后续统一从该工厂创建产品,是更清晰的边界。
完整版教学
一、多租户让“产品族”多了一层业务边界
普通抽象工厂常按平台或厂商选择产品族;多租户系统还要按租户隔离配置。租户 A 使用 AWS 新加坡区域,租户 B 使用阿里云杭州区域,租户 C 使用私有化环境,它们都可能实现同一组抽象产品接口。
Tenant A -> AwsFactory(region=ap-southeast-1)
Tenant B -> AliyunFactory(region=cn-hangzhou)
Tenant C -> PrivateCloudFactory(region=local)
同一个 StorageClient 接口背后可能有完全不同的凭证和数据边界。抽象工厂在这里不只是创建模式,还承载租户隔离和产品族一致性的结构约束。
二、FactoryKey 要覆盖所有影响创建的维度
多租户抽象工厂最容易出错的地方是缓存 key 过窄。只按 vendor 缓存,看起来能复用工厂,但会把不同租户的凭证和 region 混在一起。
record FactoryKey(
String tenantId,
String environment,
String vendor,
String region,
String credentialVersion
) {}
如果有 50 个租户、2 个环境、3 个 region,理论工厂配置组合可能达到:
50 * 2 * 3 = 300
这不表示一定要创建 300 个长生命周期工厂,但说明 key 设计必须能区分这些组合。缓存可以惰性创建和淘汰,不能用错误复用换性能。
三、请求入口确定工厂,流程内不要散读配置
一个危险写法是每个产品创建时都去配置中心读取租户信息。配置更新发生在请求中途时,同一流程可能前半段使用旧配置,后半段使用新配置。
更稳的流程:
请求进入
-> 解析 tenantId/environment
-> 构造 FactoryKey
-> 获取 CloudFactory
-> 后续 Storage/Queue/Monitor 都从同一工厂创建
请求结束
这条流程让一次请求有清晰的配置快照。配置可以在下一次请求生效,而不是在一次业务流程中间突然改变产品族。
四、配置变更时要处理旧工厂
多租户配置会变更,比如凭证轮换、region 迁移、从沙箱切生产。工厂如果持有连接池、线程池或缓存,旧工厂不能简单丢在 Map 里。
常见流程:
发现配置版本 v1 -> 创建 Factory(v1)
凭证轮换到 v2 -> 新请求使用 Factory(v2)
等待 v1 上正在执行的请求结束
关闭 Factory(v1) 持有的资源
如果每个工厂持有 4 个线程,凭证轮换 20 次但旧工厂都不关闭,就可能残留 4 * 20 = 80 个线程。配置系统和工厂缓存必须配套设计。
记忆钩子:多租户抽象工厂最怕两件事,key 少维度串租户,旧工厂不关闭泄资源。
五、环境隔离不要只靠命名约定
测试、预发、生产往往使用不同产品族或不同配置。如果只靠字符串命名,例如 "prod-aliyun"、"test-aliyun",很容易拼错或误传。更好的做法是把环境做成明确字段,并在启动阶段校验。
| 字段 | 校验示例 |
|---|---|
| environment | prod 不能使用 mock 工厂 |
| tenantId | 必须存在租户配置 |
| vendor | 必须在允许列表中 |
| region | 必须匹配厂商支持区域 |
| credentialVersion | 必须可解析且未过期 |
这些校验不是抽象工厂模式自带的,但在多环境落地时非常重要。模式负责结构,配置治理负责安全边界。
六、和依赖注入容器怎么配合
Spring 容器适合管理固定的工厂实现,例如 AwsFactoryBuilder、AliyunFactoryBuilder。但具体租户工厂可能需要按运行时 key 动态创建,不能简单用一个全局 singleton 解决。
一种做法是:
Spring 管理 FactoryBuilder
FactoryProvider 按 FactoryKey 创建/缓存具体 CloudFactory
业务服务注入 FactoryProvider
请求入口用 tenantId 获取具体工厂
这样既利用容器管理基础依赖,又保留运行时多租户选择能力。不要把所有租户工厂都硬编码成 Bean 名称,否则租户数量一多会失控。
七、常见误区与追问
- 误区:多租户只要按 vendor 选择工厂就够。 租户、环境、region、凭证版本都可能影响创建结果。
- 误区:配置更新后直接覆盖 Map 即可。 旧工厂可能仍被请求使用,还可能持有线程池和连接池。
- 误区:每个产品自己读配置更灵活。 这会破坏同一流程内的产品族一致性。
- 误区:所有租户工厂都注册成 Spring Bean 最简单。 动态租户数量大时会让容器配置膨胀,运行时 provider 更合适。
- 追问:如何防止测试环境误连生产? 环境字段显式建模,启动校验禁止 test 环境使用 prod 凭证。
- 追问:工厂缓存什么时候淘汰? 配置版本变化、租户下线、长时间未访问或资源压力达到阈值时。
- 追问:同一次请求是否允许中途切换工厂? 通常不允许,应使用请求入口确定的配置快照。
八、加强记忆
多租户抽象工厂要把“产品族一致性”扩展成“租户和环境一致性”。答题时讲清 FactoryKey、入口选厂、流程内固定、配置变更关闭旧资源、环境校验和容器配合,就能覆盖真实项目的高频追问。