2. 多租户模型与隔离策略

多租户是 IoT SaaS 平台的核心命题:同一套服务进程、同一套数据库集群、同一套消息通道,要同时承载成百上千个客户,而每个客户都要求“我的设备、我的数据、我的规则,别人看不见也改不了”。这一节围绕四个问题展开:请求进来后如何确定“这是哪个租户”、数据在存储层隔离到什么程度、哪些资源共享哪些独占、以及一个租户从开通到注销要经历哪些状态。

一、租户识别:把 tenant_id 贯穿整条链路

租户识别不是登录时做一次就结束的动作,而是要在每一次请求、每一条消息、每一次设备上报中都能被还原出来。IoT 场景的识别入口比普通 Web 系统更多,常见有四类:

  • 控制台/API 请求:从 JWT 的 claim 中取 tenant_id,或从子域名(如 acme.iot.example.com)解析,二者必须一致,否则拒绝。
  • 设备接入:设备用 ProductKey + DeviceName + DeviceSecret 连接 MQTT Broker,Broker 在鉴权阶段查出设备所属租户,把 tenant_id 写入会话上下文。
  • 消息流转:上行消息的 Topic 中携带租户前缀,例如 /t/{tenant_id}/{product_key}/{device_name}/up,规则引擎、流处理、时序写入都从 Topic 解析租户。
  • 异步任务与回调:定时任务、Webhook、离线计算必须显式携带 tenant_id,不能依赖“当前登录用户”这种隐式上下文。

工程上最稳妥的做法是:在网关层完成一次识别,把 tenant_id 注入到请求上下文(如 Java 的 ThreadLocal、Go 的 context.Context),后续所有数据访问都从上下文取,禁止业务代码自行从参数里读租户。这样能避免“参数里传了别的租户 ID 就越权”的经典漏洞。

租户识别入口与 tenant_id 注入链路 控制台 / OpenAPI JWT claim · 子域名 设备 MQTT 接入 ProductKey+DeviceSecret 上行消息 Topic /t/{tenant_id}/.../up 异步任务 / Webhook 显式携带 tenant_id 接入网关 ① 解析租户标识 ② 校验租户状态 ③ 注入上下文 tenant_id + 隔离级别 请求上下文 tenant_id isolation_level resource_pool 全链路透传 数据访问层 规则引擎 时序存储 消息通道

二、数据隔离级别:从共享表到独立库

数据隔离通常分四档,隔离强度与成本同向上升:

级别存储形态隔离强度典型适用
L1 共享表同表加 tenant_id 列,行级过滤弱,靠代码保证免费版、试用租户
L2 共享库独立 Schema同库不同 schema,如 t_acme.device中,权限可隔离标准付费租户
L3 独立数据库每租户一个 DB 实例或独立库强,故障与备份独立企业版、合规要求高
L4 独立部署独立集群/独立 VPC最强,物理隔离金融、政务、专有云

L1 的关键风险是“漏加 where 条件”。工程上不要依赖开发者自觉,而应通过 MyBatis 拦截器、JPA 的 @Filter、或数据库行级安全策略(如 PostgreSQL RLS)强制注入租户条件。以 PostgreSQL 为例:

ALTER TABLE device ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON device
  USING (tenant_id = current_setting('app.tenant_id')::bigint);
-- 每次请求开始时执行:
SET LOCAL app.tenant_id = '1024';

这样即使 SQL 忘记写租户条件,数据库也会自动过滤。IoT 的时序数据量极大,通常设备属性、遥测点走 L1 共享表 + 时间分区,而设备档案、规则配置、密钥等敏感数据按租户级别提升到 L2/L3,形成“混合隔离”。

三、共享与独立资源的权衡

隔离级别决定数据怎么放,资源共享策略决定计算和连接怎么分。IoT 平台最容易被单租户拖垮的资源有三类:MQTT 连接数、消息吞吐、时序写入。常见做法是给每个租户分配配额并做限流:

  • 连接配额:max_connections,超出后新连接被拒绝并返回明确错误码。
  • 消息速率:msg_per_sec,用令牌桶在 Broker 侧限流,避免一个租户的异常设备刷爆整个集群。
  • 存储配额:tsdb_retention_days 与 max_storage_gb,到期自动降采样或清理。

共享资源池适合中小租户,成本低、利用率高;独立资源池适合大客户,但会带来资源碎片。实践中常采用“共享池 + 大客户独占池”的混合模式,并在租户元数据中标记 resource_pool = shared | dedicated,路由层据此选择连接池和消息队列。

权衡原则:隔离级别越高,运维成本、资源浪费和跨租户统计难度越大。不要默认给所有租户最高隔离,而应按套餐、合规要求和数据敏感度分级配置。

四、租户生命周期管理

租户不是一条静态记录,而是有状态的对象。典型状态包括:申请中 → 试用 → 正式 → 欠费冻结 → 注销。每个状态对应不同的资源行为:

  • 试用:限制设备数(如 50 台)、保留期 7 天,到期自动转冻结。
  • 正式:按套餐开放配额,支持扩容。
  • 欠费冻结:禁止新设备接入和写入,但保留数据只读,给 30 天续费窗口。
  • 注销:先逻辑删除,进入 15 天冷静期,之后物理清理数据库、对象存储和密钥。

生命周期管理必须与租户识别联动:网关在注入 tenant_id 后要检查租户状态,冻结租户的请求直接返回 403 TENANT_FROZEN,而不是让它继续写入。注销流程要有幂等设计,避免清理任务重复执行导致误删其他租户数据。

常见坑:只在应用层做租户过滤,却忘了缓存、消息队列和对象存储。Redis key 要带租户前缀,Kafka Topic 或消息头要带 tenant_id,OSS/S3 路径要按租户分目录,否则隔离会出现“木桶短板”。

把四件事串起来看:租户识别解决“是谁”,隔离级别解决“数据放哪、怎么防越权”,资源共享策略解决“算力和连接怎么分”,生命周期解决“从生到死怎么管”。四者共同构成多租户平台的地基,后续的计费、监控、审计都建立在这套模型之上。