优先用ZSET+前缀编码配合ZRANGEBYLEX实现毫秒级补全;KEYS阻塞、SCAN无序低效,ZSET通过$标记词尾、\xff范围查询精准截取前缀,双ZSET架构兼顾字典序与热度排序。

直接结论:别用 KEYS 扫描,优先用有序集合(ZSET)+ 前缀编码,配合 ZRANGEBYLEX 实现毫秒级补全。
为什么不能用 KEYS 或 SCAN 做搜索补全?
很多人第一反应是“把所有关键词存成 key,然后用 KEYS prefix* 匹配”,这在测试环境可能跑得通,但上线后会拖垮 Redis。因为 KEYS 是阻塞式全量扫描,SCAN 虽非阻塞,但遍历成本随数据量线性增长,10 万关键词下平均响应常超 50ms,且无法按热度排序。
-
KEYS在生产环境必须禁用(Redis 默认关闭) -
SCAN返回结果无序,还得额外查 score 或时间戳再排序,IO 和 CPU 双重开销 - 无法支持“输入 ‘app’ 时,‘apple’ 排在 ‘application’ 前面”这类业务规则
用 ZSET 实现字典序前缀补全的实操要点
核心思路是把每个关键词拆解为所有前缀,并统一存入一个 ZSET,用特殊字符(如 $)标记词尾,靠 ZRANGEBYLEX 利用 Redis 内置的字典序范围查询能力快速截取。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 插入 “apple” 时,实际写入:
ZADD autocomplete f f$ fo fo$ foo foo$ foob fooba apple apple$ - 查询 “app” 开头的词:执行
ZRANGEBYLEX autocomplete [app [app\xff(\xff是 ASCII 最大字符,确保覆盖所有以 app 开头的字符串) - 返回结果里过滤掉中间前缀(如
app、appl),只保留带$的完整词(apple$→ 去掉 $ 得apple) - 务必设置
MAXLEN限制 ZSET 大小,否则内存持续膨胀;建议单个前缀最多存 200 条,用ZREMRANGEBYRANK定期修剪
如何让热门词排前面?
纯字典序不够,用户更希望搜 “te” 时,“test” 比 “technical” 先出现。解决方案是:不用 score 控制顺序,改用双 ZSET 架构。
- 主 ZSET(
autocomplete:prefix)只存前缀 + 词尾,负责快速定位候选集 - 热度 ZSET(
hotwords:prefix)存完整词 + score(如搜索次数),仅用于排序 - 先用
ZRANGEBYLEX拿到 50 个候选词,再用ZMSCORE批量查它们的热度分,客户端或服务端做 top-K 合并排序 - 避免在 ZSET 中混用 score 和字典序逻辑——score 会影响
ZRANGEBYLEX行为,导致前缀匹配失效
Spring Boot 中调用 ZRANGEBYLEX 的坑
Spring Data Redis 的 RedisTemplate 默认不暴露 ZRANGEBYLEX,必须用原生命令或 Lettuce 客户端直连。
- 用
RedisTemplate.execute()配RedisCallback,传 byte[] 参数(注意编码,推荐 UTF-8) - 不要用
StringRedisTemplate.opsForZSet().rangeByLex()—— 它底层调的是ZRANGEBYLEX,但对\xff处理不稳定,某些版本会截断 - 参数必须严格满足格式:
[min表示包含 min,(min表示不包含;[max同理;\xff必须是字节0xff,不是字符串 "\xff" - 示例 Java 片段:
byte[] min = ("[" + prefix).getBytes(StandardCharsets.UTF_8);<br>byte[] max = ("[" + prefix + "\u00ff").getBytes(StandardCharsets.UTF_8);<br>Set<byte[]> results = connection.zRangeByLex(key, min, max);
最易被忽略的一点:前缀编码必须全局统一,比如中文词要转拼音再切前缀,否则“北京”和“北”无法命中同一组前缀节点。这个预处理逻辑一旦漏掉,补全就变成随机匹配。

















