3. 缓存架构设计

在认证授权平台中,缓存是支撑高并发令牌校验与刷新的核心基础设施。OIDC 协议要求客户端定期刷新令牌(Refresh Token),而每次刷新都涉及对用户会话、授权范围、客户端元数据的查询。若每次刷新都穿透到数据库,平台将无法承受大规模并发。本章从缓存层次、键设计原则、失效策略三个维度,构建一个高可用、低延迟的缓存架构。

3.1 缓存层次:本地缓存与分布式缓存

认证授权平台通常采用两级缓存架构:本地缓存(L1) 与 分布式缓存(L2)。两者在延迟、容量、一致性上各有取舍,需根据令牌校验路径的读写特征进行分层部署。

维度 本地缓存(L1) 分布式缓存(L2)
存储位置 应用进程内堆内存 独立缓存集群(如 Redis、Memcached)
访问延迟 < 1ms(无网络开销) 1-5ms(网络往返)
容量上限 受限于 JVM 堆大小(通常 GB 级) TB 级,可水平扩展
数据一致性 弱一致性(各节点独立) 强一致性(单分片内)或最终一致性
典型用途 高频读取的短期令牌、公钥(JWK) 用户会话、刷新令牌、授权码
失效广播 需借助消息总线或轮询 原生支持 TTL 与主动删除
架构决策要点: 对于 OIDC 令牌刷新场景,Access Token 的签名公钥(JWK) 应放入本地缓存,因为其变化频率极低(小时/天级),且每次校验都需要读取;Refresh Token 的会话状态 必须放入分布式缓存,因为其生命周期长、数据量大,且需要跨节点一致失效。
两级缓存架构与 OIDC 令牌刷新数据流向 客户端 API 网关 认证授权服务 节点 1 L1: 本地缓存 (Caffeine) 认证授权服务 节点 2 L1: 本地缓存 (Caffeine) Redis 集群 L2: 分布式缓存 数据库 持久化存储 数据流向说明 ① 客户端请求 → 网关 ② 网关路由到服务节点 ③ 节点查 L1 → L2 → DB

3.2 缓存键设计原则

缓存键(Cache Key)的设计直接影响命中率、冲突概率和运维可读性。在认证授权平台中,键设计需遵循以下原则:

  1. 唯一性:键必须能唯一标识一个缓存条目,避免不同数据共享同一键导致脏读。
  2. 可读性:键应包含业务语义,便于在 Redis 控制台或日志中快速定位问题。
  3. 避免热点:避免使用固定前缀 + 自增 ID 的简单模式,防止 Redis 分片倾斜。
  4. 版本化:在键中嵌入数据版本号或 schema 版本,支持平滑迁移。
  5. TTL 友好:键结构应支持按业务维度批量失效(如按用户 ID 前缀扫描)。
最佳实践: 推荐使用 业务域:实体类型:标识符:版本 的四段式键结构。例如:oidc:refresh_token:usr_abc123:v2。这种结构在 Redis 中可以利用 SCAN 命令按前缀批量清理。
缓存对象 推荐键格式 示例 TTL
Refresh Token 会话 oidc:session:{sub}:{token_id} oidc:session:user_001:rt_xyz789 Refresh Token 剩余有效期
Access Token 公钥 oidc:jwk:{kid} oidc:jwk:key_rsa_001 24h(配合后台轮换)
授权码(Authorization Code) oidc:auth_code:{code_hash} oidc:auth_code:a1b2c3d4 5-10 分钟
客户端信息 oidc:client:{client_id} oidc:client:my_web_app 1h
用户授权范围 oidc:scope:{sub}:{client_id} oidc:scope:user_001:my_web_app 与会话 TTL 一致

3.3 缓存失效策略

缓存失效是认证授权平台中最容易引发故障的环节。失效策略选择不当会导致:用户明明已登出,但令牌仍被缓存视为有效(缓存穿透);或缓存频繁失效导致数据库压力激增(缓存雪崩)。以下是三种核心失效策略及其适用场景:

3.3.1 基于 TTL 的被动失效

为每个缓存条目设置过期时间(TTL),到期后自动删除。这是最基础、最可靠的策略,适用于所有缓存对象。关键点在于 TTL 的设置必须小于或等于后端数据的实际有效期。

// 设置 Refresh Token 缓存,TTL 等于令牌剩余有效期
String key = "oidc:session:" + sub + ":" + tokenId;
int ttlSeconds = refreshToken.getExpiresIn(); // 例如 3600 秒
redisTemplate.opsForValue().set(key, sessionData, ttlSeconds, TimeUnit.SECONDS);

3.3.2 基于事件的主动失效

当用户执行登出、密码修改、权限变更等操作时,由业务系统主动发送失效事件,清除相关缓存。这是保证缓存与数据库一致性的关键手段。

  • 本地缓存失效:通过消息队列(如 RabbitMQ、Kafka)广播失效事件,各节点监听后清除本地缓存。
  • 分布式缓存失效:直接调用 Redis 的 DEL 命令删除指定键,或使用 EXPIRE 命令立即过期。
// 用户登出时主动失效所有相关缓存
public void invalidateUserSession(String sub) {
    // 1. 失效分布式缓存中的会话
    String pattern = "oidc:session:" + sub + ":*";
    Set<String> keys = redisTemplate.keys(pattern);
    if (keys != null && !keys.isEmpty()) {
        redisTemplate.delete(keys);
    }
    // 2. 发送广播事件,通知各节点失效本地缓存
    eventPublisher.publishEvent(new SessionInvalidationEvent(sub));
}

3.3.3 预热与防雪崩策略

在缓存大规模失效(如重启、TTL 集中到期)时,需要防止所有请求同时穿透到数据库。常用手段包括:

  • 加随机 TTL:在基础 TTL 上增加 ±10% 的随机偏移,避免大量键同时过期。
  • 本地缓存兜底:即使分布式缓存失效,本地缓存仍能提供短时间的数据服务。
  • 限流与熔断:对数据库查询接口实施限流,防止缓存雪崩击穿数据库。
缓存失效策略决策流程 缓存失效触发 可接受 最终一致性? TTL 被动失效 + 随机偏移防雪崩 主动失效 事件广播 + 立即删除 缓存与数据库一致 监控缓存命中率 & 延迟 关键指标 • 缓存命中率 > 99% • 数据库 QPS 波动 < 20%
常见故障案例: 某平台在用户登出时仅删除了分布式缓存中的会话,但未广播事件清除各节点的本地缓存。导致用户在登出后 30 秒内(本地缓存 TTL 内)仍能使用旧 Access Token 访问资源。修复方案:在登出接口中增加消息队列广播,各节点监听后执行 localCache.invalidate(key)。

3.4 缓存架构设计检查清单

检查项 标准 验证方式
本地缓存容量 单节点 ≤ 512MB,最大条目数 ≤ 10 万 JMX 监控 / 堆转储分析
分布式缓存分片 Redis 集群至少 6 个分片,避免热点键 Redis 慢查询日志 + hotkey 分析
TTL 设置 所有缓存条目必须设置 TTL,最大不超过 24h 代码审计 + Redis 键扫描
失效广播 关键事件(登出、权限变更)必须广播到所有节点 链路追踪 + 日志聚合
缓存穿透保护 数据库查询接口必须限流,缓存空值设置短 TTL 压力测试 + 熔断指标