缓存穿透导致接口被恶意刷爆怎么办?
我们线上有个商品详情接口,最近被爬虫疯狂请求不存在的ID,直接打穿缓存压垮数据库了。试过加布隆过滤器但没生效,是不是哪里写错了?
这是我现在用的缓存逻辑:
async function getProduct(id) {
const cacheKey = <code>product:${id}</code>;
let data = await redis.get(cacheKey);
if (data !== null) return JSON.parse(data);
// 缓存未命中,查数据库
data = await db.query('SELECT * FROM products WHERE id = ?', [id]);
if (data) {
await redis.setex(cacheKey, 3600, JSON.stringify(data));
}
// 问题:如果id根本不存在,这里不会缓存空值,下次还会查库
return data;
}
先说布隆过滤器正确的用法:它应该放在查询缓存之前,判断这个id是否可能存在。如果布隆过滤器说这个id不可能存在,直接返回空就行了,根本别去查库和缓存。你的代码里如果是在缓存未命中后才判断布隆过滤器,那肯定没用,该穿透的还是穿透。
具体这样改:
第一步,把布隆过滤器放到最前面。查缓存之前先问布隆过滤器:「这个id有可能存在吗?」如果不存在,直接返回空。
第二步,缓存空值。对不存在的id也写入缓存,但是存一个特殊标记,比如
NULL或者{},过期时间设短一点,比如1-5分钟。这样下次再请求同样的无效id,直接从缓存拿到空值,不会打库。第三步,接口限流。恶意爬虫你光靠缓存防是不够的,得在入口限流。用redis做个简单的计数器,同一个IP或者同一个id请求超过阈值就拒绝。或者直接上现成的限流中间件。
第四步,id校验。如果你的商品id是有规律的(比如必须是数字、长度范围固定),在入口先过滤掉明显不合法的请求,减轻后面所有逻辑的压力。
代码改完大概长这样:
核心就三点:布隆过滤器放最前面判断、空值也要缓存、配合限流。基本上能解决你这个问题。
你现在的代码里,对不存在的 ID 完全没缓存,爬虫一刷不存在的 ID 就直奔数据库,肯定扛不住。先最简单的补救:对查不到的数据也缓存个空值,但过期时间短点,比如 5 分钟。这样恶意刷不存在 ID 的请求最多打一次库,后面都命中空缓存。
修改后的代码:
这样至少能挡住 90% 的无效请求。
至于布隆过滤器没生效,大概率是你没提前把所有合法 ID 都塞进去,或者布隆过滤器本身容量太小、哈希函数设计不合理。布隆过滤器适合提前拦截明显不存在的 ID,但它有误判率,不能完全依赖它防穿透,得配合上面的空值缓存一起用。
如果你数据量特别大(比如百万级商品),布隆过滤器可以放 Redis 前面一层,比如用 Redisson 的布隆过滤器实现:
但注意:布隆过滤器只适合 ID 已知、变动不频繁的场景。如果商品是动态新增的,得保证布隆过滤器实时更新,否则会误杀新商品。
总结:先上空值缓存,立竿见影;布隆过滤器作为进阶防护,但别指望它 alone 能解决问题。