Redis缓存穿透和缓存击穿本质上是两种不同的资源耗尽型攻击路径,但很多团队在防护时容易混为一谈。穿透是利用“不存在的Key”绕过缓存直接打数据库;击穿是针对“某一个高热Key过期瞬间”的并发冲击。两者的共同后果都是数据库瞬时压力过大,但触发条件、攻击成本、防护手段完全不同。如果不区分场景套用同一套方案,反而会留下新的漏洞。下面直接拆解具体的攻击原理和可落地的防护策略。
缓存穿透的触发机制与核心风险
缓存穿透的典型特征是请求查询一个数据库中根本不存在的Key。因为缓存里没有,每次请求都会穿透到数据库执行一次查询,而数据库也查不到结果,自然不会回填缓存。攻击者只要构造大量不存在的Key,比如随机负数ID、超长随机字符串、不存在的商品编号,就能让缓存命中率趋近于零,所有压力全部落到数据库。更隐蔽的做法是利用不同参数组合绕过业务校验,例如对同一接口传入格式合法但语义非法的ID。数据库每秒钟可能被几万甚至几十万次无效查询打满连接池,最终拖垮整个服务。
缓存穿透的第一层防护:参数校验与数据形态过滤
在网关层或业务入口处做严格的参数合法性校验,是成本最低也最容易被忽略的一步。比如ID必须是正整数、长度必须在一定范围内、字符串必须符合特定正则规则。很多穿透攻击之所以成功,就是因为业务接口只判断了参数是否存在,没有判断参数形态是否合法。对于商品ID、用户ID这类主键,可以直接在接入层用正则或类型判断拦截明显非法的请求。这一步能过滤掉大量随机生成的无效Key,但不能完全解决语义合法但数据库不存在的Key。
缓存穿透的第二层防护:缓存空值标记
对于通过参数校验但数据库确实不存在的Key,可以在缓存中写入一个空值标记,并设置较短的过期时间,例如30秒到5分钟。这样同一个无效Key在短时间内不会反复打到数据库。实现上需要注意两点:一是空值标记要使用特殊值,不能和正常业务数据混淆,比如用字符串“NULL”或特定的负数标识;二是过期时间不宜过长,否则可能因为业务数据后续真实生成而出现短暂的数据不一致。空值缓存适合无效Key重复率较高的场景,但如果攻击者不断变换Key,这种方案的防护效果会明显下降。
缓存穿透的第三层防护:布隆过滤器前置拦截
布隆过滤器是解决缓存穿透最硬核的手段之一。它可以在内存中维护一个高效的存在性判断结构,将所有合法的Key提前加载到布隆过滤器中。请求进来时先经过布隆过滤器,如果判断Key一定不存在,直接返回空结果,根本不会进入缓存和数据库。布隆过滤器存在一定的误判率,即可能把不存在的Key判断为存在,但绝不会把存在的Key判断为不存在。误判率可以通过调整位数组大小和哈希函数数量来控制。实际部署时需要注意,布隆过滤器需要支持动态扩容和增量更新,否则业务新增的合法Key无法被识别。对于数据量极大的系统,可以考虑使用Redis的Bitmap实现分布式布隆过滤器,或者使用RedisBloom模块。
缓存击穿的触发机制与高危场景
缓存击穿是指某个被超高并发访问的热点Key在缓存中过期或被删除的瞬间,大量请求同时涌入数据库查询同一个Key。与穿透不同,这个Key在数据库中确实存在,而且往往是秒杀商品、热门新闻、明星动态这类访问量极高的数据。一旦缓存失效,数据库可能在几百毫秒内收到数万次相同的查询请求。即使数据库单次查询很快,连接池也会被瞬间占满,导致其他正常请求无法获取连接,形成雪崩效应。击穿攻击不一定是恶意的,正常的流量高峰也可能触发,但攻击者可以通过监控热点Key的过期时间,选择在过期瞬间发起集中请求来放大破坏效果。
缓存击穿的核心防护:互斥锁与逻辑过期
互斥锁方案是最直接的解决思路。当缓存失效后,只允许一个请求去数据库查询并回填缓存,其他请求等待锁释放后直接从缓存读取。实现上可以使用Redis的SETNX命令或分布式锁组件。需要注意的是,锁的粒度要足够细,一般以Key为单位,避免不同Key之间互相阻塞。同时要设置锁的超时时间,防止持有锁的线程异常退出导致死锁。下面是一段基于Redis的互斥锁伪代码:
String getData(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 10); // 10秒超时
if (locked) {
try {
value = db.query(key);
redis.set(key, value, 60);
} finally {
redis.del(lockKey);
}
} else {
Thread.sleep(50);
return getData(key); // 重试
}
return value;
}
互斥锁方案的优点是实现简单、数据一致性强,但在高并发下会产生大量重试和线程等待,对请求线程的占用较大。另一种更优的方案是逻辑过期。逻辑过期不真正删除缓存中的Key,而是在缓存值中附加一个过期时间戳。读取时如果发现时间戳已过期,先返回旧值,同时异步启动一个线程去更新缓存。这样所有请求都能立即拿到数据,不会阻塞,数据库压力也被限制在单个异步任务内。逻辑过期方案牺牲了短暂的数据新鲜度,但换取了极高的并发吞吐能力,适合热点数据的读多写少场景。
热点Key的主动识别与预热策略
防护缓存击穿不能只靠被动应对,还需要主动识别热点Key并提前预热。可以在业务层统计Key的访问频率,将访问量排名靠前的Key标记为热点数据。对于已知的热点Key,设置永不过期或极长的过期时间,并通过后台任务定期刷新。对于突发热点,例如突发事件带来的流量洪峰,可以在监控到访问量异常升高时,动态延长该Key的过期时间,甚至临时改为不过期。预热策略的关键在于提前将热点数据加载到缓存中,避免在流量高峰时才去数据库查询。对于秒杀类活动,可以在活动开始前几分钟将商品详情、库存等数据提前写入缓存,并设置合理的过期时间覆盖整个活动周期。
多级缓存与限流降级的组合防御
单靠Redis一层缓存无法完全抵御大规模攻击,引入本地缓存作为二级缓存可以进一步降低Redis和数据库的压力。本地缓存可以使用Caffeine或Guava Cache,将热点数据缓存在应用进程内,读取速度更快,且不占用网络开销。当Redis出现异常或被打满时,本地缓存仍然可以支撑一部分请求。同时,在网关层或业务层增加限流和降级策略,对单个Key的访问频率进行限制,超过阈值的请求直接返回降级结果或默认值,避免请求继续向后端穿透。限流可以基于令牌桶或滑动窗口算法实现,降级结果可以是静态兜底数据或友好的错误提示。这种多层组合防御能够将攻击流量逐层消化,最终到达数据库的请求量会被控制在可承受范围内。
监控告警与应急响应机制
任何防护方案都需要配合完善的监控体系才能发挥最大价值。需要重点监控的指标包括:Redis缓存命中率、数据库连接池使用率、单个Key的访问频率、缓存过期事件数量、锁等待时间等。当缓存命中率突然下降或数据库连接数异常升高时,应该立即触发告警。应急响应方面,可以准备一键切换开关,在检测到穿透或击穿攻击时,快速启用空值缓存、延长热点Key过期时间、开启全局限流或切换到降级模式。同时,保留攻击期间的请求日志,分析攻击Key的规律和来源特征,为后续的策略优化提供依据。
方案落地时的常见误区
很多团队在实施防护时容易陷入几个误区。第一,把所有Key都设置成不过期,虽然避免了击穿,但会导致缓存中堆积大量冷数据,内存成本急剧上升。第二,布隆过滤器初始化后不再更新,导致新数据被误判为不存在,影响正常业务。第三,互斥锁的超时时间设置不合理,锁过早释放导致多个请求同时回源,或者锁过晚释放导致请求大量超时。第四,只关注技术防护而忽略业务层面的数据校验,攻击者可以通过构造合法但不存在的业务数据绕过技术防线。正确的做法是根据业务场景分层设防,每一层解决特定类型的问题,而不是寄希望于单一方案解决所有风险。
Redis缓存穿透和击穿的防护本质上是一场资源消耗的对抗。攻击者的目标是让有限的数据库连接和计算资源被无效请求耗尽,而防御方的目标是在请求到达数据库之前尽可能多地识别和拦截无效流量。参数校验、空值缓存、布隆过滤器解决的是穿透问题,互斥锁、逻辑过期、热点预热解决的是击穿问题,多级缓存和限流降级提供的是兜底保障。只有把这些手段按照业务特点组合起来,并配合持续的监控和应急演练,才能真正把缓存层的安全防线做实。
