在中小学教育 SaaS 的运维日历中,每年 9 月初的“开学季心理普查”是技术团队必须面对的“年度渡劫”。
全市 500 所学校、几十万名学生会在教委统一规定的某一天、甚至是同一个早自习的 45 分钟内,集中涌入平台开始疯狂做题。
在这种极具毁灭性的洪峰流量面前,任何直接将请求打向 MySQL 的架构都会在一分钟内被撕成碎片。
为了保命,架构师们都会在 MySQL 前面挡上一层内存数据库——Redis。把学校信息、量表题库这些极少变动的数据全部放进 Redis,让 MySQL 在大后方安心处理“保存答卷”的写入操作。
然而,如果缓存策略设计得不够极客,Redis 依然会成为整个系统的死亡陷阱。本文将硬核复盘缓存雪崩与穿透的真实场景,以及那些绝地反击的防御代码。
缓存雪崩(Cache Avalanche):过期时间的致命共振
假设技术团队在 9 月 1 日凌晨,通过一段脚本将 500 套心理量表和各大学校的配置信息,全部预热到了 Redis 中,并极其统一地设置了“24 小时后过期”。
灾难发生在了 9 月 2 日的凌晨。
那 24 小时的倒计时结束了。成千上万个极度重要的缓存 key 在同一秒钟极其整齐地集体失效(被 Redis 自动清理)。
如果在这一瞬间,正好有几千个熬夜做测试的请求打过来,它们去 Redis 里一摸,发现什么都没有!于是这几千个请求如同决堤的洪水,瞬间全部穿过 Redis,狠狠地砸在毫无防备的 MySQL 身上。MySQL 的 CPU 瞬间飙到 100%,连接池被打爆,全站宕机。
极客的解药:时间的随机离散
永远不要给批量导入的缓存设置完全一样的过期时间。
正如橙星云研发团队在缓存网关中植入的铁律:在设置 TTL 时,必须在基础时间之上,强行叠加一段极其随机的时间(如 1~60 分钟的随机数)。
“`java
// 基础过期 24 小时 (86400秒) + 极其随机的打散时间 (0~3600秒)
int expireTime = 86400 + new Random().nextInt(3600);
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
“`
通过这种微秒级的降维打散,原本会在同一秒爆发的雪崩,被极其均匀地分摊到了一个小时内,彻底化解了瞬时的击穿危机。
缓存穿透(Cache Penetration):黑客的致命狙击
相比于雪崩的内伤,缓存穿透往往是遭到恶意狙击的结果。
正常情况下,学生查询自己的报告,请求 id=1024。先查 Redis,如果没有,再去查 MySQL。
但如果有人刻意编写了极具攻击性的脚本,疯狂请求那些根本不存在的数据(比如 id=-1 或者 id=999999999)呢?
因为这个 ID 本来就是假的,Redis 里肯定没有;接着请求穿透到 MySQL,MySQL 一顿全表扫描,发现也没有,最后返回空。
由于这笔假数据既不在 Redis 也不在 MySQL,下一次这个假请求打过来,依然会结结实实地再砸 MySQL 一次。黑客靠着一堆假 ID,就能直接把数据库活活耗死。
极客的解药:布隆过滤器(Bloom Filter)的绝对拦截
针对这种恶意刺探,技术团队祭出了最硬核的数据结构核武器:布隆过滤器。
我们在 Redis 前面,再加一层由极其紧凑的位图(BitMap)和多个哈希函数构成的布隆过滤器。
所有真实的报告 ID,都会在这个位图中被标记。
当黑客带着 id=-1 杀过来时,布隆过滤器在一微秒内进行几次哈希运算,极其冰冷地告诉网关:“这个位图里绝对没有这个 ID”。
请求甚至连 Redis 的门都碰不到,就被当场无情丢弃,更别提去打扰极其脆弱的 MySQL 了。
(如果少量真实数据因为布隆过滤器的极低误判率被挡,只需提供一个极隐蔽的强制刷新接口即可。)
总结
在应对开学季这种极度变态的脉冲流量时,Redis 绝不仅仅是个简单的内存 Hash 字典。
通过在时间维度上注入随机扰动化解雪崩,在空间维度上部署布隆过滤器粉碎穿透攻击,技术团队用这些极其精密的极客策略,将汹涌狂暴的并发流量死死地挡在了应用层的护城河外。在这场算力与并发的殊死搏斗中,唯有严密的架构才能镇压一切恐慌。
