多租户隔离与资源配额:共享 Redis 的边界设计

Redis 多租户隔离与资源配额实战:独享实例与共享实例逻辑隔离模型对比、DB 编号在 Cluster 下的限制、key 前缀与 ACL 按模式授权、maxmemory 全局限制与租户级配额统计、热点租户的大 Key 与热 Key 归因、慢租户拖垮整实例的防护、按前缀的用量统计与计费、选型建议

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 不支持多 DBCluster 模式只有 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 有三个必须知道的限制:

  1. 不限制资源:ACL 只管「能不能访问」,不管「能占多少内存」「能打多少 QPS」。配额需要另做。
  2. 模式匹配有开销:key 模式在每次命令执行时校验,通配符过多会增加 CPU 开销。
  3. 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 的最终形态。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. Key 设计与命名规范:Redis 里唯一的结构约束
  2. 代理与路由方案:客户端直连之外的另一种选择
  3. Kubernetes Operator 运维:Redis 集群的声明式管理