2. Redis 核心数据结构在鉴权场景中的典型用途

在构建后端鉴权拦截层时,Redis 并非仅仅充当“缓存”,其五种核心数据结构各自对应着不同的鉴权子问题。理解每种结构的特性与适用边界,是设计高效、安全拦截机制的前提。

2.1 数据结构选型总览

数据结构 底层实现 鉴权核心用途 典型命令
String 动态字符串 (SDS) 单点 Token 存储、Session ID 映射、API Key 值查询 SET / GET / SETEX / GETSET
Hash 压缩列表 / 哈希表 用户属性缓存、OAuth2 授权码元数据、角色-权限映射 HSET / HGET / HGETALL / HEXISTS
Set 整数集合 / 哈希表 黑名单、白名单、已撤销 Token 集合、Scope 集合 SADD / SISMEMBER / SMEMBERS / SCARD
ZSet 跳表 + 哈希表 Token 过期排序、限流滑动窗口、最近活跃用户排行 ZADD / ZRANGEBYSCORE / ZREMRANGEBYSCORE
选型原则: 单值查 String,多字段查 Hash,集合判存在用 Set,排序/范围淘汰用 ZSet。不要用 String 拼接存储多字段,不要用 Set 做范围查询。

2.2 String:Token 与 Session 的原子载体

String 是 Redis 最基础的结构,在鉴权中承担“键-值”直查场景。其 SETEX 命令可原子性地设置值并附加 TTL,天然适合存储一次性登录凭证或短期 Access Token。

// 用户登录成功后,生成 Token 并写入 Redis
SETEX "auth:token:u_1001" 3600 "eyJhbGciOiJIUzI1NiIs..."
// 鉴权拦截时直接 GET 查询
GET "auth:token:u_1001"
// 若返回 nil,说明 Token 已过期或不存在,直接拒绝
性能指标: 单节点 GET 操作延迟通常在 0.1ms 以内(内网环境),QPS 可达 10 万+。String 是鉴权路径上延迟最低的数据结构。

2.3 Hash:用户属性与授权元数据的结构化存储

当需要缓存用户的多项鉴权属性(如角色、部门、权限版本号、登录设备指纹)时,Hash 比多个 String 更节省内存,且支持单字段更新。

// 存储用户鉴权属性
HSET "auth:user:1001" "role" "admin" "dept" "engineering" "perm_version" "42"
// 鉴权时仅获取所需字段
HGET "auth:user:1001" "role"
// 判断字段是否存在
HEXISTS "auth:user:1001" "perm_version"

在 OAuth2 授权码模式中,授权码的元数据(client_id、redirect_uri、scope、用户 ID)非常适合用 Hash 存储,过期时间通过单独的 EXPIRE 命令控制。

2.4 Set:黑/白名单与 Scope 集合的 O(1) 判存在

Set 的无序、不重复特性使其成为鉴权中“集合判存在”场景的首选。无论是已撤销 Token 列表、IP 黑名单,还是允许访问的 Scope 集合,SISMEMBER 命令都能在 O(1) 时间内给出结果。

// 将已撤销的 Token 加入黑名单
SADD "auth:token:revoked" "token_jti_abc123"
// 鉴权时快速判断
SISMEMBER "auth:token:revoked" "token_jti_abc123"
// 返回 1 表示已撤销,直接拦截
注意: Set 不适合存储大量过期数据。若黑名单持续增长,需配合定期清理或使用 ZSet 按时间淘汰。Set 的 SMEMBERS 命令在集合较大时可能阻塞 Redis,生产环境应避免频繁调用。

2.5 ZSet:Token 过期排序与限流滑动窗口

ZSet 的 score 排序特性在鉴权中有两个关键用途:按时间范围淘汰和滑动窗口计数。

场景一:Token 自动过期清理
将 Token 的过期时间戳作为 score,定期使用 ZREMRANGEBYSCORE 清理已过期的 Token,避免内存泄漏。

// 存储 Token 及其过期时间戳
ZADD "auth:token:expire" 1718000000 "token_jti_abc123"
ZADD "auth:token:expire" 1718003600 "token_jti_def456"
// 定时任务:移除所有 score 小于当前时间戳的成员
ZREMRANGEBYSCORE "auth:token:expire" 0 1718000000

场景二:接口限流滑动窗口
将每次请求的时间戳作为 score,统计指定时间窗口内的请求次数。

// 用户 1001 的限流窗口
ZADD "auth:ratelimit:1001" 1718000001 "req_001"
ZADD "auth:ratelimit:1001" 1718000002 "req_002"
// 统计 1 秒窗口内的请求数
ZCOUNT "auth:ratelimit:1001" 1718000000 1718001000

2.6 数据结构组合:鉴权拦截的完整数据流

实际生产中的鉴权拦截并非依赖单一数据结构,而是多结构协同。下图展示了一次完整请求的 Redis 数据流:

鉴权拦截 Redis 数据结构协同流程 鉴权拦截 Redis 数据结构协同流程 HTTP 请求到达 ① String: GET auth:token:{jti} Token 是否存在?是否过期? ② Set: SISMEMBER auth:token:revoked Token 是否已被撤销? ③ Hash: HGET auth:user:{uid} role 获取用户角色与权限版本 ④ ZSet: ZCOUNT auth:ratelimit:{uid} 滑动窗口内请求是否超限? 放行 / 拦截决策 每一步返回 nil 或 0 时立即拦截,避免后续无效查询

2.7 数据结构选型避坑清单

  • 不要用 String 存储多字段 JSON: 更新单个字段需要整体读写,并发下易丢失更新。应使用 Hash 按字段操作。
  • 不要用 Set 做范围查询: Set 只支持随机遍历和判存在,无法按时间或分数排序。需要淘汰时用 ZSet。
  • 不要忽略 ZSet 的内存开销: 每个 ZSet 成员约占用 40+ 字节(含跳表指针),大量 Token 场景需评估内存预算。
  • 不要混合存储不同业务的数据: 使用统一前缀命名空间(如 auth:token:、auth:user:),避免键冲突和误操作。
  • 不要依赖 Redis 的过期通知做鉴权: Redis 的 Key 过期通知(Pub/Sub)并非实时可靠,鉴权拦截应主动查询 TTL 或使用 ZSet 主动清理。
最佳实践: 在鉴权拦截中间件中,将 Redis 查询结果缓存到本地请求上下文(如 ThreadLocal 或请求结构体),避免同一请求内多次重复查询同一 Key。例如,Token 解析出的用户 ID 只需查一次 Hash 即可复用。