SaaS 系统天然多租户。十个客户共用一套代码、一套数据库,Redis 自然也想共用——但共享意味着一个租户的行为会影响其他租户:某个客户跑了一次全量导出,KEYS * 把主线程阻塞三秒,所有客户一起超时;某个客户的缓存膨胀到 8 GB,把实例内存打满,触发了 maxmemory 淘汰,其他客户的缓存被无辜驱逐。
隔离的本质是在共享与独占之间画一条线。线画得太靠共享,成本低但风险高;画得太靠独占,成本爆炸。本文给出三种隔离模型的取舍框架,拆解 Redis 原生能力能做到什么程度,以及哪些能力必须靠代理层或应用层补齐。
一、三种隔离模型
| 模型 | 隔离级别 | 成本 | 适用 |
|---|---|---|---|
| 独享实例 | 进程/网络级 | 最高(每租户一套) | 大客户、合规要求 |
| 共享实例 + 逻辑隔离 | 逻辑级(前缀/ACL) | 低 | 中小客户、长尾 |
| 混合分层 | 分级 | 中 | 大多数 SaaS 的最终形态 |
1.1 独享实例
每个租户一套独立的 Redis(或 Cluster),物理隔离。
优点:
- 故障完全隔离:一个租户的慢查询、内存爆满、误删操作都影响不到别人。
- 配额简单:
maxmemory就是该租户的硬上限,超了就淘汰自己的数据。 - 合规友好:数据物理分离,审计与数据出境要求容易满足。
- 可定制:不同租户可以用不同的
maxmemory-policy、持久化策略、版本。
缺点:
- 成本线性增长:100 个租户就是 100 个实例,即使每个只用了 10 MB,也要占一份基础内存开销(Redis 空实例约 3~5 MB)与连接数。
- 运维复杂度:100 个实例的监控、备份、升级、故障处理。
- 资源利用率低:大部分租户的实例长期处于低负载。
1.2 共享实例 + 逻辑隔离
所有租户共用一个实例(或 Cluster),靠 key 前缀与 ACL 做逻辑隔离。
优点:成本低、资源利用率高、运维集中。
缺点:没有硬隔离——一个租户的异常行为会影响所有租户,这是必须正视的风险。
1.3 混合分层
实践中几乎都是混合:按租户等级分池。
P0(大客户,付费高)→ 独享实例,独立 Cluster
P1(中客户) → 共享实例池 A(8 个实例,按租户哈希分配)
P2(长尾小客户) → 共享实例池 B(2 个实例,逻辑隔离 + 强配额)
这样既控制成本,又给关键客户足够保障。多租户架构的整体设计原则可参考 多租户架构设计 ,Redis 层只是其中一环。
二、逻辑隔离的三种手段
2.1 DB 编号:看上去很美,Cluster 下不可用
Redis 默认提供 16 个数据库(databases 16),用 SELECT n 切换:
SELECT 0
SET user:1 a
SELECT 1
SET user:1 b # 与 db0 的 user:1 是两个不同的键
看起来是天然的租户隔离,但有几个致命问题:
| 问题 | 说明 |
|---|---|
| Cluster 不支持多 DB | Cluster 模式只有 db0,SELECT 1 直接报错 |
| 无配额隔离 | 所有 DB 共享同一个 maxmemory |
| 无慢查询隔离 | 一个 DB 的慢命令阻塞所有 DB |
| 过期策略共用 | 无法按 DB 配置不同策略 |
| 运维工具支持差 | 很多工具只操作 db0 |
| 官方不推荐 | Redis 官方明确建议用 key 前缀替代 DB 编号 |
结论:不要用 DB 编号做租户隔离。 它在单机模式下勉强可用,但一旦要上 Cluster(几乎必然),整个方案就要推倒重来。
2.2 key 前缀:唯一可行且必须的基础
tenant:1001:user:88:profile
tenant:1001:order:detail:8899
tenant:1002:user:88:profile
前缀隔离是所有方案的基础,它带来三个能力:
- 可统计:按前缀统计用量(见第六节)。
- 可清理:
SCAN MATCH tenant:1001:*可以定位该租户的所有 key。 - 可授权:ACL 可以按 key 模式授予权限(见下)。
前缀的层级设计要与 key 规范一致:租户 ID 应该放在最外层,保证 tenant:<id>: 是稳定的可枚举前缀,后续的实体与标识再依次向后排。
2.3 ACL:把前缀变成权限边界
Redis 6 引入 ACL,Redis 7 增强了 key 模式匹配。这是逻辑隔离里唯一由服务端强制执行的机制:
# 为租户 1001 创建独立用户,只允许访问自己的前缀
ACL SETUSER tenant_1001 on >strongpass1001 \
~tenant:1001:* \
&tenant:1001:* \
+@read +@write +@keyspace -@dangerous
# 关键部分:
# ~tenant:1001:* 只允许读写匹配该模式的 key
# &tenant:1001:* 只允许订阅匹配该模式的 Pub/Sub 频道
# +@read +@write 允许读写命令
# -@dangerous 禁止 FLUSHALL/KEYS/CONFIG 等
验证权限:
# 用该用户连接
redis-cli --user tenant_1001 --pass strongpass1001
# 访问自己的 key:成功
GET tenant:1001:user:88
# 访问别人的 key:被拒绝
GET tenant:1002:user:88
# (error) NOPERM this user has no permissions to access one of the keys used as arguments
# 危险命令:被拒绝
KEYS *
# (error) NOPERM this user has no permissions to run the 'keys' command
ACL 的配置细节(用户管理、ACL FILE 持久化、ACL LOG 审计)见 生产安全指南
。
但 ACL 有三个必须知道的限制:
- 不限制资源:ACL 只管「能不能访问」,不管「能占多少内存」「能打多少 QPS」。配额需要另做。
- 模式匹配有开销:key 模式在每次命令执行时校验,通配符过多会增加 CPU 开销。
- Cluster 下的槽位限制:
~tenant:*这种宽模式在 Cluster 下无法限制到具体槽位,租户的数据仍然散落在所有分片。
| 能力 | key 前缀 | ACL | 代理层 |
|---|---|---|---|
| 数据访问隔离 | 否(仅约定) | 是(强制) | 是 |
| 内存配额 | 否 | 否 | 需自建 |
| QPS 限流 | 否 | 否 | 是 |
| 命令白名单 | 否 | 是 | 是 |
| 用量统计 | 需扫描 | 可审计 | 是 |
这张表说明了逻辑隔离的真实边界:ACL 能挡住「越权访问」,但挡不住「资源挤占」。后者必须靠代理层或应用层。
三、资源配额:Redis 没有租户级配额
Redis 的 maxmemory 是实例级的。没有「租户 A 最多用 500 MB」这样的原生配置。要实现租户配额,只有三条路。
3.1 路线一:按前缀统计,超限告警或清理
统计某个租户的用量:
# 方案 A:SCAN 遍历(准确但慢,仅适合离线统计)
redis-cli --scan --pattern 'tenant:1001:*' --count 500 | wc -l
# 方案 B:MEMORY USAGE 逐个测量(更慢,仅适合抽样)
redis-cli --scan --pattern 'tenant:1001:*' --count 100 | head -20 | \
xargs -I{} redis-cli MEMORY USAGE {}
SCAN 是游标迭代,不阻塞主线程,但在千万级 key 上遍历一次仍然很慢。实践做法是离线定时统计(如每小时一次),落到监控系统里,超限时告警。
更高效的方案是应用侧计数:每次写 key 时维护一个租户计数器(HINCRBY tenant:usage 1001 <bytes>),但精确维护成本高,通常用估算。
3.2 路线二:代理层配额
在代理层拦截请求,按 key 前缀识别租户,累加字节数或 QPS,超限时拒绝。这是唯一能做到实时硬配额的位置。
请求到达代理
→ 解析 key,提取租户 ID(前缀 tenant:<id>:)
→ 查询该租户的配额与当前用量
→ 未超限:转发;超限:返回 -ERR tenant quota exceeded
实现要点:
- 用量计数要放在代理本地内存(如滑动窗口),避免每次请求都访问 Redis 造成递归依赖。
- 配额变更要能热更新(从配置中心推送)。
- 计数要定期与离线统计对账,防止漂移。
代理层的能力边界与选型需要单独评估,核心是确认它能否按 key 前缀识别租户并做实时拒绝。
3.3 路线三:限流而非限额
内存配额难以精确控制,但QPS 限流是成熟且容易实现的:
-- 按租户限流的 Lua 脚本(滑动窗口)
local key = KEYS[1] -- rate:tenant:1001
local limit = tonumber(ARGV[1]) -- 每秒上限
local now = tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', key, 0, now - 1000)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
end
redis.call('ZADD', key, now, now .. ':' .. math.random())
redis.call('PEXPIRE', key, 2000)
return 1
限流能间接控制内存增长速度:写入速率被限制,膨胀速度也就被限制了。限流算法的完整对比见 高并发限流器设计 。
| 配额类型 | 实现难度 | 精度 | 推荐 |
|---|---|---|---|
| 内存配额(硬) | 高(需代理) | 高 | 大客户 |
| 内存配额(软,离线统计+告警) | 低 | 中 | 大多数场景 |
| QPS 限流 | 低 | 高 | 必做 |
| 连接数限制 | 低(代理 maxclients) | 高 | 必做 |
| Key 数量上限 | 中(需定期统计) | 中 | 可选 |
四、热点租户:共享池里最常见的事故
共享实例的故障大多来自同一个模式:某个租户的流量或数据规模突然放大,挤占其他租户的资源。
4.1 大 Key 的租户归因
# 找出所有大 Key,并提取租户 ID
redis-cli --bigkeys
# 输出示例:
# Biggest hash found 'tenant:1001:product:cache' has 1200000 fields
# 按租户聚合大 Key 数量
redis-cli --bigkeys | grep '^Biggest' | awk '{print $3}' | \
cut -d: -f2 | sort | uniq -c | sort -rn
识别出热点租户后,处理路径有两条:要求该租户拆 Key(治本),或者把该租户迁移到独享实例(隔离)。大 Key 的拆分方法论见 大 Key 与热 Key 治理 。
4.2 热 Key 的租户归因
热 Key 的识别靠 redis-cli --hotkeys(需要 maxmemory-policy 为 LFU 类)或 MONITOR 抽样:
redis-cli --hotkeys
# 输出示例:
# [45.12%] Hot key 'tenant:1001:hot:config' found so far
按前缀归因后,可以对该租户做针对性限流,或引导其使用本地缓存,把读压力从 Redis 转移到应用进程内。
4.3 慢租户:比热点更隐蔽
热 Key 是「读得多」,慢租户是「命令重」。典型的慢命令:
| 命令 | 风险 | 替代 |
|---|---|---|
KEYS * | O(N) 阻塞 | SCAN |
HGETALL(大 Hash) | 返回巨量数据 | HSCAN |
SMEMBERS(大 Set) | 同上 | SSCAN |
SORT(大 List) | O(N log N) | 预排序或改结构 |
ZRANGE key 0 -1 | 全量返回 | 分页 ZRANGE key 0 99 |
FLUSHALL | 清空全库 | 禁止(ACL -@dangerous) |
防护手段:ACL 屏蔽危险命令(服务端强制)+ 代理层拦截重命令(可解析参数长度)+ 慢查询日志告警。三者叠加才能覆盖「无意写错」与「恶意刷量」两类来源。
五、监控与计费
多租户场景下,监控必须按租户维度拆开,否则无法定位「是谁在捣乱」。
5.1 按前缀的用量统计
# 用 Lua 脚本原子统计某租户的 key 数量与内存
redis-cli EVAL "
local cursor = '0'
local count = 0
local bytes = 0
repeat
local res = redis.call('SCAN', cursor, 'MATCH', ARGV[1], 'COUNT', 200)
cursor = res[1]
for _, k in ipairs(res[2]) do
count = count + 1
bytes = bytes + redis.call('MEMORY', 'USAGE', k)
end
until cursor == '0'
return {count, bytes}
" 0 'tenant:1001:*'
这个脚本在百万级 key 上会跑很久,不要在线上直接执行——应该在从节点上跑,或者拆成分批任务。
5.2 关键监控指标
| 指标 | 来源 | 租户维度 | 说明 |
|---|---|---|---|
| Key 数量 | 离线 SCAN | 按前缀 | 内存增长趋势 |
| 内存占用 | MEMORY USAGE 汇总 | 按前缀 | 配额依据 |
| QPS | 代理指标 | 按前缀 | 限流依据 |
| 慢命令数 | SLOWLOG | 需归因 | 定位慢租户 |
| 大 Key 数 | --bigkeys | 按前缀 | 治理依据 |
| 连接数 | CLIENT LIST | 按用户(ACL) | ACL 用户与租户一一对应时可用 |
CLIENT LIST 配合 ACL 用户是很好的归因手段:
redis-cli CLIENT LIST | grep 'user=tenant_1001'
# 输出该租户的所有连接及其命令统计
这要求每个租户用独立的 ACL 用户连接,而不是所有租户共用一个账号。这是 ACL 带来的额外收益:不仅隔离权限,还隔离了连接与统计维度。
进一步的归因手段是给每个租户的连接打上 CLIENT SETNAME:
CLIENT SETNAME tenant_1001_app1
redis-cli CLIENT LIST | grep 'name=tenant_1001'
连接名与 ACL 用户双重标记后,CLIENT LIST 的输出可以直接按租户聚合,排查「哪个租户的连接数暴涨」时不需要再翻代码。
5.3 计费口径
如果按用量计费,需要明确计量什么:
| 计费维度 | 采集方式 | 特点 |
|---|---|---|
| 存储量(GB·小时) | 定时快照内存占用 | 与成本最相关 |
| 请求数(万次) | 代理层计数 | 易采集、易作弊 |
| 带宽(GB) | 代理层流量统计 | 对 Redis 成本影响大 |
| 峰值 QPS | 代理层滑动窗口最大值 | 用于容量规划 |
推荐以存储量 + 请求数双维度计费,因为这两者直接对应内存与 CPU 成本。
六、选型建议
| 租户规模 | 数据量 | 建议模型 |
|---|---|---|
| < 50 个 | 单租户 < 100 MB | 共享实例 + 前缀 + ACL + 限流 |
| 50~500 个 | 单租户 < 1 GB | 共享实例池(按租户哈希分池)+ 软配额 |
| > 500 个 | 单租户差异大 | 混合分层:大客户独享,长尾共享 |
| 任意规模 | 单租户 > 10 GB | 独享实例(或独享 Cluster) |
| 合规要求高 | 任意 | 独享实例 |
一条经验线:当单个租户的数据量超过实例 maxmemory 的 10% 时,就应该考虑把它移出共享池。因为它的一次批量写入就可能触发全局淘汰,影响所有其他租户。
6.1 从共享池迁出单个租户
当一个租户需要从共享池迁移到独享实例时,迁移过程要保证「应用不改代码」。步骤:
1. 新建独享实例,配置与共享池一致的 maxmemory-policy 与持久化策略
2. 用 ACL 创建同名用户 tenant_<id>,权限范围与共享池一致
3. 双写阶段:应用同时写共享池与新实例(需要代码支持,或用代理层做影子写)
4. 用 SCAN + RESTORE 把存量 key 搬迁到新实例
5. 读流量切换:把该租户的读请求指向新实例,观察错误率
6. 写流量切换:确认无异常后停掉双写
7. 清理共享池中该租户的残留 key
第 3 步的双写是整个过程最重的部分。如果不改代码,替代方案是在代理层做透明路由:把 tenant:<id>:* 的请求整体指向新实例,存量数据靠离线搬迁补齐,切换瞬间用短暂只读窗口兜住差异。
6.2 什么时候该放弃共享
三种情况下共享方案的成本会超过收益:
| 信号 | 说明 |
|---|---|
| 租户数量少但单个规模大 | 共享的规模效应不存在,反而要额外做配额 |
| 有强合规要求 | 数据物理隔离是硬要求,逻辑隔离无法满足审计 |
| 故障影响面不可接受 | 一次事故影响所有客户,赔偿成本高于硬件成本 |
反过来,如果租户数量多、单个规模小、SLA 容忍度较高,共享池是最优解——这正是长尾客户的典型画像。
七、生产实践清单
- 不要用 DB 编号做隔离,Cluster 不支持多 DB。
- 所有 key 强制
tenant:<id>:前缀,这是统计、清理、授权的基础。 - 每个租户一个 ACL 用户,用
~tenant:<id>:*限定 key 模式,用-@dangerous屏蔽危险命令。 - 共享池必须做 QPS 限流,这是成本最低、收益最高的防护。
- 内存配额靠离线统计 + 告警,需要硬配额时上代理层。
- 定期跑
--bigkeys与--hotkeys,按前缀归因到租户。 - 监控必须按租户维度拆分,否则无法定位故障源。
- 单租户数据量超过
maxmemory的 10% 时,迁移到独享实例。
小结
Redis 没有为多租户提供原生支持,逻辑隔离必须由「key 前缀 + ACL + 代理层」三者拼出来。ACL 是唯一服务端强制的机制,但它只解决「能不能访问」,解决不了「能占多少资源」——配额与限流必须自己实现,这是共享方案最容易被低估的成本。
选型上抓住两条线就够:单个租户的数据量是否超过实例 maxmemory 的 10%?合规或 SLA 是否要求故障完全隔离?两个都是「否」,共享实例加前缀加 ACL 加限流就是性价比最高的方案;任何一个为「是」,就该把该租户迁到独享实例。混合分层不是折中,而是大多数 SaaS 的最终形态。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。