4. 缓存穿透与缓存击穿:OAuth2.0场景下的空令牌查询与过期瞬间的并发冲击

在OAuth2.0令牌缓存的高并发场景中,缓存穿透与缓存击穿是两类极易引发Redis热点甚至雪崩的典型故障。前者源于对不存在令牌的反复查询,后者则发生在令牌恰好过期、大量请求同时回源的瞬间。本章将结合OAuth2.0的业务特征,剖析这两种问题的成因、危害,并给出针对性的优化策略。

4.1 缓存穿透:空令牌查询的“无底洞”

缓存穿透指查询一个一定不存在的令牌(如伪造的access_token、已撤销的refresh_token),由于缓存中永远无法命中,请求每次都会穿透到后端数据库或授权服务器,导致资源浪费甚至压垮下游。

4.1.1 OAuth2.0场景下的穿透特征

  • 恶意攻击:攻击者构造大量随机字符串作为令牌,绕过缓存直接查询DB。
  • 令牌撤销:已撤销的令牌在缓存中不存在,但客户端仍可能携带它发起请求。
  • 过期清理:Redis的过期策略(如主动删除+惰性删除)无法保证100%实时清理,但空令牌查询仍会穿透。

4.1.2 穿透的危害量化

指标正常命中穿透场景恶化倍数
QPS(查询/秒)10,00010,0001x
Redis命中率99%0%—
DB回源QPS10010,000100x
平均响应延迟2ms200ms+100x
关键结论:穿透场景下,DB回源QPS可能从100飙升至10,000,直接导致数据库连接池耗尽、授权服务不可用。

4.2 缓存击穿:过期瞬间的“并发洪峰”

缓存击穿指一个热点令牌(如某个高并发客户端的access_token)在缓存过期的一瞬间,大量并发请求同时发现缓存失效,全部回源查询数据库,造成瞬时压力集中。

4.2.1 OAuth2.0场景下的击穿特征

  • 热点令牌集中:例如某第三方应用的access_token被数千个微服务同时使用。
  • 过期时间一致:令牌的TTL(如3600秒)导致大量请求在同一秒内发现缓存失效。
  • 回源成本高:授权服务器验证令牌需要签名校验、黑名单检查、数据库查询,耗时远高于缓存命中。

4.2.2 击穿与穿透的对比

维度缓存穿透缓存击穿
查询对象不存在的令牌存在但刚过期的热点令牌
触发条件恶意构造/撤销清理TTL到期+并发回源
持续时间持续攻击期间过期后极短时间(ms级)
影响范围所有空令牌查询单个热点令牌
典型后果DB持续高负载瞬时DB洪峰+服务抖动

4.3 两种问题的并发冲击路径

下图展示了OAuth2.0网关在令牌校验时,缓存穿透与击穿导致的请求回源路径差异:

缓存穿透与击穿并发冲击路径对比 缓存穿透与击穿并发冲击路径对比 客户端请求 OAuth2.0网关 Redis缓存 授权服务器/DB 正常查询 命中返回 穿透:空令牌→直接回源DB 并发请求群 缓存过期 DB瞬时洪峰 所有请求同时发现缓存过期 全部回源 冲击路径总结: • 穿透:空令牌 → 网关直接回源DB → DB持续高负载(无缓存参与) • 击穿:热点令牌过期 → 并发请求同时回源 → DB瞬时洪峰(ms级冲击)

4.4 优化策略:从源头阻断冲击

4.4.1 缓存穿透的解决方案

  • 布隆过滤器(Bloom Filter):在网关层维护一个布隆过滤器,存储所有有效令牌的指纹。查询前先判断令牌是否存在,若不存在则直接拒绝,避免穿透到Redis和DB。
  • 缓存空对象:对于查询结果为空(如已撤销令牌),在Redis中缓存一个短TTL的空值(如60秒),后续相同查询直接返回空,不再穿透。
  • 参数校验前置:在网关层对令牌格式(如JWT结构、长度、签名算法)进行校验,过滤明显非法的请求。
实践建议:布隆过滤器适合“存在性判断”场景,误判率可控制在1%以下;空对象缓存适合应对突发穿透,但需注意不要缓存过久导致数据不一致。

4.4.2 缓存击穿的解决方案

  • 互斥锁(Mutex Lock):当缓存失效时,只允许一个线程回源查询并重建缓存,其他线程等待缓存重建完成。常用实现:Redis的SETNX命令或Redisson分布式锁。
  • 逻辑过期(提前续期):在缓存中存储令牌的“逻辑过期时间”,后台异步线程在逻辑过期前主动刷新缓存,避免热点令牌在业务高峰期过期。
  • 热点令牌永不过期:对于识别出的高并发热点令牌,设置“永不过期”策略,通过后台任务定期更新令牌内容,彻底消除过期瞬间的并发冲击。

4.4.3 策略对比与选型

策略适用场景复杂度额外成本一致性保证
布隆过滤器穿透(空令牌)低内存占用(约1MB/百万令牌)强(误判可接受)
缓存空对象穿透(临时空值)极低Redis内存(短TTL)弱(短暂不一致)
互斥锁击穿(热点过期)中锁等待延迟强
逻辑过期击穿(可预测热点)中高后台线程/定时任务最终一致
永不过期击穿(极高热点)低后台更新任务最终一致

4.5 实战案例:互斥锁防止击穿

以下代码展示了在OAuth2.0令牌校验中,使用Redis SETNX实现互斥锁防止缓存击穿的典型逻辑:

// 伪代码:OAuth2.0令牌校验 + 互斥锁防击穿
public TokenInfo validateToken(String accessToken) {
    // 1. 先查缓存
    TokenInfo token = redis.get("token:" + accessToken);
    if (token != null) {
        return token;
    }
    
    // 2. 缓存未命中,尝试获取分布式锁
    String lockKey = "lock:token:" + accessToken;
    boolean locked = redis.setnx(lockKey, "1", 5, TimeUnit.SECONDS);
    
    if (locked) {
        try {
            // 3. 双重检查:可能其他线程已重建缓存
            token = redis.get("token:" + accessToken);
            if (token != null) {
                return token;
            }
            
            // 4. 回源授权服务器验证令牌
            token = authServer.verifyToken(accessToken);
            
            // 5. 重建缓存(设置过期时间)
            if (token != null) {
                redis.setex("token:" + accessToken, 3600, token);
            } else {
                // 空对象缓存,防止穿透
                redis.setex("token:" + accessToken, 60, null);
            }
            return token;
        } finally {
            redis.del(lockKey);
        }
    } else {
        // 6. 未获取到锁,等待并重试
        Thread.sleep(50);
        return validateToken(accessToken); // 递归重试
    }
}
注意事项:互斥锁方案会增加一次Redis SETNX操作的开销,且锁超时时间需合理设置(建议50-200ms),避免锁长期占用导致请求堆积。对于极高并发场景,可结合“逻辑过期”策略进一步降低锁竞争。

4.6 本章小结

  • 缓存穿透:空令牌查询导致请求直达DB,危害持续且范围广;推荐布隆过滤器+空对象缓存组合防御。
  • 缓存击穿:热点令牌过期瞬间引发并发回源,危害集中且突发性强;推荐互斥锁或逻辑过期策略。
  • 工程落地:需根据令牌的访问热度、过期时间分布、系统容错能力综合选型,避免单一策略的局限性。