布隆过滤器用于拦截非法ID请求,通过预热合法ID实现存在性快筛,误判率宜设为0.01;需配合带过期时间的显式空值缓存(如"NULL")和接口层ID格式校验,并监控Redis未命中与空值过期指标。

用布隆过滤器拦截非法ID请求
爬虫全量遍历的本质,是构造大量不存在的 user:123456789、order:999999999 这类key发起查询——它们既不在缓存,也不在数据库。布隆过滤器(Bloom Filter)就是干这个的:在请求真正触达缓存/数据库前,先做一次「存在性快筛」。
它不保证100%准确(有极小误判率),但能以极低内存开销挡住99.9%的无效请求。关键在于:误判只可能“说存在但实际上不存在”,绝不会“说不存在但其实存在”——所以它只用于拒绝,不用于放行决策。
- 初始化时必须预热:从数据库加载所有合法
id(如SELECT id FROM users),逐个调用bfAdd加入过滤器 - 误判率别设太低(如
0.001):会导致 bitmap 过大,Redis 内存暴涨;生产常用0.01(1%),配合百万级容量已足够 - 别用纯内存布隆(如 Guava BloomFilter):它无法跨进程共享,多实例部署下会漏判;必须用 RedisBloom 模块或自建 Redis bitmap + 多 hash 位校验
- 示例逻辑:
if !rb.bfExists('user_filter', user_id):<br> return {'code': 404, 'msg': '用户不存在'}
空值缓存必须带过期时间且区分标记
布隆过滤器挡不住误判的那1%,也防不了新注册用户尚未入库就被查的场景。这时候需要第二道防线:缓存空结果,但不能简单存 null 或空字符串。
问题在于:如果所有空值都统一用 "",后续业务层无法判断这是“真没数据”还是“缓存占位符”。更糟的是,若忘记设过期时间,等于永久缓存了“不存在”,导致数据上线后长期查不到。
- 务必用显式标记对象,比如
"NULL"字符串或序列化后的NullObject类实例 - 过期时间要短:一般
60秒足够,太长会拖慢新数据生效;太短(如5秒)则起不到缓冲作用 - 别在业务代码里重复写
setex:封装成统一方法,强制传ttl参数,避免漏设 - 示例:
redis.setex("user:999999", 60, "NULL")
接口层加基础 ID 校验,别等进缓存再拦
布隆和空缓存都是“事后防御”,而最省资源的方式,是在第一公里就掐断明显非法请求。比如爬虫常生成负数ID、超长数字、含字母的伪ID,这些根本没必要走缓存链路。
这类校验成本几乎为零,却能过滤掉大量低级遍历流量,还能顺便规避部分越权尝试。
- 对数字型ID:检查是否 > 0、是否在合理范围(如
user_id ) - 对字符串型ID(如 UUID):用正则粗筛格式,
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i - 别只在校验层 throw 异常:返回明确 HTTP 状态码(如
400 Bad Request),方便网关或 WAF 统计拦截率 - 别和业务逻辑耦合太紧:校验应放在 Controller 或网关层,而非 Service,否则绕过 API 直连缓存时就失效了
监控命中率与空值命中趋势,早于报警发现穿透苗头
布隆+空缓存不是一劳永逸。当某类 key 的缓存未命中率突然飙升,同时 NULL 缓存写入量激增,大概率说明有新一波遍历开始——可能换了策略,或绕过了某层校验。
靠人工看日志太慢,必须把指标接入可观测体系,让数据自己说话。
- 重点盯两个指标:
redis_keyspace_misses(Redis 未命中数)、redis_db_keys_expired(空值 key 过期数),二者同步上涨是强信号 - 按 key pattern 聚合统计:比如
user:* → NULL的写入频次,比全局指标更有定位价值 - 别只看平均值:用分位数(如 P95)看长尾请求,爬虫常集中在某几个 ID 段反复扫
- Prometheus + Grafana 是最低成本方案,
redis_exporter开箱即用,无需改业务代码
布隆过滤器的预热时机、空值缓存的 TTL 设置粒度、ID 校验的严格程度——这三者没有标准答案,得看你的数据分布、QPS 峰值和容忍延迟。最容易被忽略的,其实是监控告警的响应闭环:指标异常了,谁来查布隆过滤器是否漏加了新表?谁来确认空缓存是不是该延长 TTL?这些事,代码写不完。










