96SEO 2026-04-21 04:50 60
说实话,缓存穿透这四个字,在面试题里出现的时候,大家dou觉得挺简单的,不就是“查不到就去查库,库也没有就返回空”嘛。但真当这事儿发生在生产环境,尤其是凌晨三点把你从被窝里揪起来的时候,那滋味可就一点dou不好受了。

上周我就经历了这么一遭。手机狂响,迷迷糊糊接起
这些ID在数据库里压根就没有,Redis里自然也是空的。结果就是Redis成了摆设,请求像穿甲弹一样直接穿透了缓存层,全部打在了可怜的MySQL身上。这就是典型的缓存穿透。今天咱们不整那些虚头巴脑的理论,就结合这次踩坑的经历,好好聊聊怎么把这事儿给平了。
先搞清楚:为什么会被“穿透”?正常情况下咱们查数据的流程是这样的:先去Redis问一声“有这个key吗?”,Redis说有,直接返回;Redis说没有,那就去MySQL查,查到了再回写给Redis。这逻辑没毛病。
但问题就出在“MySQL里也没有”这个环节。当攻击者不停地用一个不存在的ID,比如“user_-999”或者“user_abc”来请求你时Redis查不到,MySQL也查不到。按照常规逻辑,MySQL返回空,程序也就返回空了。Zui要命的是你通常不会把这个“空结果”写到Redis里去。
于是乎,每一次请求,dou会重复“Redis未命中 -> MySQL查询 -> 返回空”的流程。几万次请求下来MySQL不挂才怪。这就好比你家门口有个推销员,每隔一分钟按一次门铃,你每次dou去开门发现没人,但他就是不停止按门铃,Zui后你肯定得累趴下。
第一道防线:把“空”也缓存起来Zui直觉、成本Zui低的方案,就是当MySQL查不到数据时我们也往Redis里写点东西,比如一个特殊的值“NULL”,或者一个空对象。这样下次再来查这个ID,Redis直接返回“NULL”,就不用去烦MySQL了。
这个思路虽然简单,但代码写起来还是有点讲究的,特别是要区分“key不存在”和“key存在但值是空标记”。
public User getUserById {
String key = "user:" + userId;
// 1. 先去 Redis 碰碰运气
String cached = redisTemplate.opsForValue.get;
if {
// 这里是个关键点:Ru果命中了还得kankan是不是咱们存的那个“空标记”
if ) {
return null; // 既然是空标记,说明数据库里肯定没有,直接返回,别去查库了
}
return JSON.parseObject;
}
// 2. Redis 确实没有,只Neng硬着头皮查 MySQL
User user = userMapper.selectById;
if {
// 正常数据,老规矩,写进 Redis,过期时间设个 30 分钟
redisTemplate.opsForValue.set, 30, TimeUnit.MINUTES);
} else {
// 重点来了:查不到数据,咱们也缓存一个“NULL”,但过期时间别太长!
redisTemplate.opsForValue.set;
}
return user;
}
这里有个大坑,千万别踩
我一开始图省事,把这个空值的过期时间设成了30分钟,跟正常数据一样。结果业务那边反馈说新注册的用户有时候查不到信息。原因hen尴尬:这个新用户ID之前被攻击者请求过Redis里缓存了“NULL”,结果人家真注册了数据写进MySQL了但Redis里的“NULL”还没过期,系统依然认为这个用户不存在。
所以空值的缓存时间一定要短,比如2分钟或者5分钟。这算是在“防止数据库被打爆”和“保证数据一致性”之间找个平衡。当然这个方案也有个致命弱点:Ru果攻击者每次dou换个随机ID,比如UUID,那Redis里瞬间就会被塞满几百万个“NULL”key,内存直接爆表。这时候,光靠这招就不灵了。
第二道防线:布隆过滤器既然被动防守有点吃力,那咱们就主动出击。布隆过滤器就是干这个用的。它的原理简单说就是:在请求到达Redis之前,先过一道安检。
我们把所有合法的、存在的用户ID,提前初始化到布隆过滤器里。当请求过来时先问布隆过滤器:“这个ID存在吗?”
Ru果布隆过滤器说“不存在”,那就绝对不存在,直接返回错误,连Redisdou不用查,MySQLgeng别想碰。
Ru果布隆过滤器说“存在”,那可Neng存在,这时候再去查Redis和MySQL。
这就把99%的非法请求挡在了门外。RedisBloom模块现在用起来挺方便的,不用自己造轮子。
import redis
import json
r = redis.Redis
# 假设我们要存 100 万个用户 ID,误判率控制在 1%
# 这一步通常在系统启动时或者通过定时任务Zuo初始化
r.execute_command
def init_bloom_filter:
# 把数据库里所有的 ID dou捞出来塞进过滤器
all_user_ids = fetch_all_user_ids_from_db
pipe = r.pipeline
for uid in all_user_ids:
pipe.execute_command)
pipe.execute
print} 个 ID")
def get_user:
# 第一关:先过布隆过滤器
exists = r.execute_command)
if not exists:
# 过滤器说没这人,那就肯定没这人,直接拦截
return None
# 第二关:查 Redis 缓存
cache_key = f"user:{user_id}"
cached = r.get
if cached:
return json.loads
# 第三关:查 DB
user = db.query_user
if user:
r.setex)
return user
布隆过滤器的那些“坑”
这东西虽然好用,但用不好也是给自己挖坑。
坑一:数据丢了怎么办?
Ru果你用的是RedisBloom模块,数据会随着RDB/AOF持久化,重启没问题。但Ru果你是用Guava这种本地内存实现的布隆过滤器,服务一重启,过滤器就空了。这时候所有请求dou会被判定为“不存在”,直接被拦截,那业务就彻底瘫痪了。所以生产环境尽量用Redis自带的模块,或者重启后赶紧预热重建。
坑二:没法删除。
布隆过滤器有个特性,它不支持删除操作。Ru果一个用户注销了他的ID还在过滤器里这倒没啥大问题,顶多就是多查一次Redis。但Ru果你的业务场景数据删除hen频繁,那就要考虑用Cuckoo Filter了或者定期全量重建一次过滤器。我现在的Zuo法是每天凌晨闲的时候,跑个任务重建一次虽然笨了点,但稳。
坑三:别忘了同步新数据。
这个坑我踩过。新用户注册的时候,只写了DB,忘了往布隆过滤器里加。结果新用户一注册,立马去查自己的信息,直接被布隆过滤器给拦了提示“用户不存在”。用户肯定要骂娘啊。所以写数据的时候,一定要记得同步geng新过滤器。
# 新用户注册时的完整流程
def register_user:
# 1. 写入数据库
user_id = db.insert_user
# 2. 同步写入布隆过滤器
r.execute_command)
# 3. 预热缓存
r.setex)
return user_id
第三道防线:Zui基础但Zui容易被忽视的参数校验
咱们搞技术的,有时候容易想太复杂,反而忽略了Zui简单的东西。在代码的Zui入口处,加一层参数校验,Neng挡掉一大半的脑残攻击。
比如我们的用户ID是数据库自增的Long类型,肯定大于0。Ru果请求里传个负数,或者传个超大的数,甚至传个带字母的字符串,那直接在Controller层就给它怼回去,别让它往下走。
@RestController
public class UserController {
@GetMapping
public Result getUser Long id) {
// 1. 参数合法性校验 —— 这一步Zui省事,也Zui有效
if {
return Result.fail;
}
// 2. 限流:同一个 IP 每秒Zui多 5 次请求
// 这里Ke以用 Guava 的 RateLimiter 或者 Redis + Lua 脚本实现
// 3. 走正常的缓存查询逻辑
User user = userService.getUserById;
return Result.success;
}
}
那次事故里攻击者用的ID千奇百怪,什么负数、UUID字符串dou有。Ru果一开始就加了校验,至少Neng过滤掉50%以上的无效流量,给后面的防线减轻不少压力。
终极方案:多层防御,别指望一招鲜经过这么一折腾,我的体会是:安全这事儿,没有银弹。指望某一个方案就Neng解决所有问题,那是在Zuo梦。
现在的架构,我一般是这么组合的:
入口层: Zuo严格的参数校验和限流,把明显不合法的请求直接拒之门外。
过滤层: 上布隆过滤器,把肯定不存在的Key拦截掉,保护Redis和MySQL。
缓存层: 即使到了这一步,对于查不到的数据,也缓存短时间的空值,防止漏网之鱼反复冲击。
这三层叠加上去,再遇到类似的攻击流量,MySQL的QPS基本稳如老狗,纹丝不动。
Zui后啰嗦一句,面试的时候,这三个词总是被放在一起问,但它们完全是不同的场景。穿透是“查不到”,击穿是“热点过期”,雪崩是“集体过期”。别混为一谈。今天这篇只聊了穿透,剩下的咱们改天再细说。希望你们别在凌晨三点接到报警
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback