根本原因是高频鉴权请求未做本地缓存兜底,导致Redis连接池耗尽;引入Caffeine缓存后90%+请求不再打Redis,连接数回落至安全水位。
网关鉴权耗尽 Redis 连接,根本原因不是 Redis 性能差,而是高频请求反复穿透到 Redis 层——每个请求都查一次 token、权限、白名单,没做本地缓存兜底。加 Caffeine 本地缓存后,90%+ 的鉴权请求不再打 Redis,连接数直接回落到安全水位。
为什么网关鉴权会压垮 Redis 连接池
Spring Cloud Gateway 默认不带鉴权缓存,每次
头解析后,都要查 Redis 判断 token 是否有效、角色是否有权限、是否在黑名单里。当 QPS 上万时,哪怕单次 Redis 查询只要 1ms,每秒也要建立上万个连接(或复用连接但触发大量
命令),而默认的 Lettuce 连接池
很快就排队阻塞,出现
或超时异常。
鉴权是「读多写少」场景:token 有效期内基本不变,但每毫秒都在被重复校验
Redis 是网络调用,哪怕同机房 RTT 也稳定在 0.3–0.8ms;Caffeine 查内存是纳秒级,差三个数量级
网关是流量入口,一旦 Redis 阻塞,整个链路雪崩,错误率飙升,和数据库慢查询一个后果
Caffeine 缓存鉴权数据的关键配置点
不是所有数据都适合放本地缓存,token、用户权限这类「变更低频、失效明确」的数据才合适。重点不是缓存大小,而是过期策略必须和业务语义对齐:
:token 本身有效期 30 分钟,本地缓存不能比它长,否则可能返回已过期却未清理的权限
不要只配
:鉴权是被动触发,
不代表 token 仍有效,容易导致脏数据滞留
务必开启
:上线后用
监控命中率,低于 85% 就要检查 key 设计或过期时间是否合理
缓存 key 要包含租户/环境维度,比如
,避免测试环境 token 冲突污染生产缓存
网关过滤器里怎么嵌入两级缓存逻辑
在自定义
中实现「先查 Caffeine → 未命中再查 Redis → Redis 命中则回填 Caffeine」,注意三个易错点:
Redis 8.2.3
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
下载
不要在
返回
后直接 fallback 到 Redis —— 必须加双重检查锁(如
),否则并发请求会重复刷 Redis
Redis 查询结果为
(比如 token 已删除)也要写入 Caffeine,设短过期(如 30 秒),防止缓存穿透,key 可标记为
如果 Redis 查询失败(连接超时、序列化异常),不要抛异常中断请求,应降级走直连后端鉴权,同时记录告警日志,避免网关因 Redis 故障整体不可用
示例片段:
本地缓存失效时的一致性陷阱
用户登出、权限变更、token 强制下线这些操作,只会删 Redis 里的 key,Caffeine 本地缓存并不感知——这是最隐蔽的「脏读」来源。解决方法不是强同步,而是轻量兜底:
对敏感操作(如管理员踢人),额外发一条
到 Redis,各网关实例订阅该 channel 并主动
更低成本的做法:在 Caffeine 配置里加上
,让缓存每两分钟自动异步重载一次,牺牲一点实时性换一致性
绝对不要用「全量清除
」来应对单个 token 失效——这会瞬间击穿全部本地缓存,效果比不加缓存还差
真正难的不是加 Caffeine,而是让缓存行为和业务生命周期对齐:token 过期时间、权限变更频率、运维强删动作,都得映射到缓存的
、
、
三类操作上。漏掉任意一环,缓存就从加速器变成定时炸弹。
AuthorizationGETmax-active: 20Cannot obtain redis connectionexpireAfterWrite(30, TimeUnit.MINUTES)expireAfterAccessaccessrecordStats()localCache.stats().hitRate()"auth:prod:token:" + md5(token)GlobalFiltercache.getIfPresent(key)nullcache.get(key, k -> loadFromRedis(k))null"auth:null:" + tokenObject authData = localCache.get(token, t -> {
Object fromRedis = redisTemplate.opsForValue().get("auth:" + t);
return fromRedis != null ? fromRedis : NULL_PLACEHOLDER;
});PUBLISH auth:invalidate token_abclocalCache.invalidate(token_abc)refreshAfterWrite(2, TimeUnit.MINUTES)localCache.invalidateAll()expirerefreshinvalidate