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 的会话状态 必须放入分布式缓存,因为其生命周期长、数据量大,且需要跨节点一致失效。
3.2 缓存键设计原则
缓存键(Cache Key)的设计直接影响命中率、冲突概率和运维可读性。在认证授权平台中,键设计需遵循以下原则:
- 唯一性:键必须能唯一标识一个缓存条目,避免不同数据共享同一键导致脏读。
- 可读性:键应包含业务语义,便于在 Redis 控制台或日志中快速定位问题。
- 避免热点:避免使用固定前缀 + 自增 ID 的简单模式,防止 Redis 分片倾斜。
- 版本化:在键中嵌入数据版本号或 schema 版本,支持平滑迁移。
- 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% 的随机偏移,避免大量键同时过期。
- 本地缓存兜底:即使分布式缓存失效,本地缓存仍能提供短时间的数据服务。
- 限流与熔断:对数据库查询接口实施限流,防止缓存雪崩击穿数据库。
常见故障案例: 某平台在用户登出时仅删除了分布式缓存中的会话,但未广播事件清除各节点的本地缓存。导致用户在登出后 30 秒内(本地缓存 TTL 内)仍能使用旧 Access Token 访问资源。修复方案:在登出接口中增加消息队列广播,各节点监听后执行
localCache.invalidate(key)。
3.4 缓存架构设计检查清单
| 检查项 | 标准 | 验证方式 |
|---|---|---|
| 本地缓存容量 | 单节点 ≤ 512MB,最大条目数 ≤ 10 万 | JMX 监控 / 堆转储分析 |
| 分布式缓存分片 | Redis 集群至少 6 个分片,避免热点键 | Redis 慢查询日志 + hotkey 分析 |
| TTL 设置 | 所有缓存条目必须设置 TTL,最大不超过 24h | 代码审计 + Redis 键扫描 |
| 失效广播 | 关键事件(登出、权限变更)必须广播到所有节点 | 链路追踪 + 日志聚合 |
| 缓存穿透保护 | 数据库查询接口必须限流,缓存空值设置短 TTL | 压力测试 + 熔断指标 |