ZSet不能防止生僻词击穿,因其仅是排序工具,不提供存在性判断;防击穿需前置过滤(格式校验、白名单)、布隆过滤器拦截及归一化空值缓存组合策略。

搜索建议场景下,ZSet 本身不能防止生僻词击穿;它只是排序工具,真正要防击穿,得靠前置过滤或空值缓存策略。
为什么 ZSet 不解决击穿问题
ZSet(zset)在搜索建议中常用于按热度排序关键词,比如 zadd search_suggestions 100 "java"。但它不参与“是否存在”的判断逻辑——请求一个不存在的生僻词(如 "xqz9823"),ZSet 查不到就返回空,后端仍会继续查数据库或 ES,和缓存穿透本质一致。ZSet 的 zscore、zrank 都是 O(log N) 查询,但查不到 ≠ 拦截请求。
- ZSet 是「结果容器」,不是「存在性过滤器」
- 没有内置机制区分“真不存在”和“暂时没收录”,更不提供布隆过滤器那样的概率拦截能力
- 如果直接用
zrangebylex或zrank判断词是否存在,高并发下大量 miss 仍会穿透到下游
生僻词请求如何提前拦截(推荐组合方案)
搜索建议的生僻词击穿,核心是「用户输入了极低频甚至非法的 query,系统却照单全收并层层往下查」。必须在进入 ZSet 查询前就做裁决:
- 第一层:格式校验 + 白名单前缀 —— 拒绝明显非法输入,如纯数字、超长串、含控制字符、非 UTF-8 编码等;对中文搜索可限制最小字数(≥2)、对英文限制最小有效词长(≥3)
-
第二层:布隆过滤器(Bloom Filter) —— 初始化时将所有已收录的 suggestion 关键词(如
"java","spring boot")加入 RedisBloom 的bf.add;请求来时先bf.exists,返回false直接 400 或返回空数组,不碰 ZSet 和 DB -
第三层:ZSet 查询 + 空值缓存兜底 —— 仅当布隆过滤器返回
true(可能存在)时才查 ZSet;若zscore返回 nil,再缓存一个空值(如setex sug:xqz9823 60 "{}"),TTL 设为 60 秒,避免同一生僻词反复穿透
空值缓存写法要注意 key 和 TTL 的粒度
直接缓存 "sug:" + rawQuery 是最直白的做法,但容易被恶意构造海量不同生僻词打爆内存。更稳妥的方式是:
- 对原始 query 做归一化:小写、去首尾空格、合并连续空格、截断超长部分(如只取前 20 字符)
- 加哈希前缀:用
md5(query)或sha256(query).substr(0,12)生成固定长度 key,避免 key 过长且可控 - TTL 不用统一值:高频误输词(如拼错的
"recat")可设 5 分钟;完全随机串("a1b2c3d4e5")设 30 秒,靠监控日志动态调整 - 慎用大 value:空值缓存用
""或"null"即可,别存 JSON 结构体,否则内存浪费翻倍
真正难处理的不是 ZSet 怎么用,而是怎么让「根本没资格进 ZSet 的请求」在第一毫秒就被拦住。布隆过滤器 + query 归一化 + 短 TTL 空值,三者缺一不可;单独依赖 ZSet 的任何操作,都等于没设防。











