缓存穿透导致接口被恶意刷爆怎么办?

司马春豪 阅读 42

我们线上有个商品详情接口,最近被爬虫疯狂请求不存在的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;
}
我来解答 赞 6 收藏
二维码
手机扫码查看
2 条解答
Newb.艳花
你的问题很清楚了,代码里确实没缓存空值,但这只是表面原因。布隆过滤器没生效大概率是用错了位置。

先说布隆过滤器正确的用法:它应该放在查询缓存之前,判断这个id是否可能存在。如果布隆过滤器说这个id不可能存在,直接返回空就行了,根本别去查库和缓存。你的代码里如果是在缓存未命中后才判断布隆过滤器,那肯定没用,该穿透的还是穿透。

具体这样改:

第一步,把布隆过滤器放到最前面。查缓存之前先问布隆过滤器:「这个id有可能存在吗?」如果不存在,直接返回空。

第二步,缓存空值。对不存在的id也写入缓存,但是存一个特殊标记,比如 NULL 或者 {},过期时间设短一点,比如1-5分钟。这样下次再请求同样的无效id,直接从缓存拿到空值,不会打库。

第三步,接口限流。恶意爬虫你光靠缓存防是不够的,得在入口限流。用redis做个简单的计数器,同一个IP或者同一个id请求超过阈值就拒绝。或者直接上现成的限流中间件。

第四步,id校验。如果你的商品id是有规律的(比如必须是数字、长度范围固定),在入口先过滤掉明显不合法的请求,减轻后面所有逻辑的压力。

代码改完大概长这样:

async function getProduct(id) {
// 先用布隆过滤器判断这个id是否可能存在
if (!bloomFilter.mightContain(id)) {
return null; // 不可能存在,直接返回
}

const cacheKey = product:${id};
let data = await redis.get(cacheKey);
if (data !== null) {
// 这里要区分是真的有数据还是空值标记
if (data === 'NULL') return 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));
} else {
// 关键:缓存空值,过期时间短一点
await redis.setex(cacheKey, 60, 'NULL');
}

return data;
}


核心就三点:布隆过滤器放最前面判断、空值也要缓存、配合限流。基本上能解决你这个问题。
点赞
2026-03-13 17:10
技术晓萌
试试这个方法:缓存空值 + 布隆过滤器双保险,别光靠一个。

你现在的代码里,对不存在的 ID 完全没缓存,爬虫一刷不存在的 ID 就直奔数据库,肯定扛不住。先最简单的补救:对查不到的数据也缓存个空值,但过期时间短点,比如 5 分钟。这样恶意刷不存在 ID 的请求最多打一次库,后面都命中空缓存。

修改后的代码:

async function getProduct(id) {
const cacheKey = product:${id};
let data = await redis.get(cacheKey);
if (data !== null) {
// 如果缓存里是空对象或者特殊标记,直接返回 null
if (data === 'NULL') return 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));
} else {
// 关键:对空结果也缓存,防止穿透
await redis.setex(cacheKey, 300, 'NULL'); // 5 分钟过期
}

return data;
}


这样至少能挡住 90% 的无效请求。

至于布隆过滤器没生效,大概率是你没提前把所有合法 ID 都塞进去,或者布隆过滤器本身容量太小、哈希函数设计不合理。布隆过滤器适合提前拦截明显不存在的 ID,但它有误判率,不能完全依赖它防穿透,得配合上面的空值缓存一起用。

如果你数据量特别大(比如百万级商品),布隆过滤器可以放 Redis 前面一层,比如用 Redisson 的布隆过滤器实现:

const redis = new Redis(...);
const rBloom = redis.getBloomFilter('product-id-bloom');
await rBloom.addAsync('123'); // 启动时把所有合法 ID 加进去
// 查询前先判断
const可能 = await rBloom.containsAsync(id);
if (!可能) return null; // 肯定不存在,直接返回
// 否则再走缓存逻辑


但注意:布隆过滤器只适合 ID 已知、变动不频繁的场景。如果商品是动态新增的,得保证布隆过滤器实时更新,否则会误杀新商品。

总结:先上空值缓存,立竿见影;布隆过滤器作为进阶防护,但别指望它 alone 能解决问题。
点赞 5
2026-02-24 14:27