在高并发搜索场景中,搜索建议(Suggester) 是用户搜索体验的核心功能。但由于 completion suggester 仍需访问 Lucene FST 结构,高频前缀(如 “a”、“ap”)的查询仍可能成为性能瓶颈。
为此,我们设计一套 Elasticsearch 搜索建议缓存系统,通过 多级缓存 + 预加载 + 智能失效 机制,实现 毫秒级响应、高吞吐、低延迟 的搜索建议服务。
一、目标与核心需求
| 目标 | 说明 |
|---|---|
| ⚡ 低延迟 | P99 < 10ms |
| 📈 高吞吐 | 支持每秒数万 QPS |
| 🧱 可扩展 | 支持多租户、多业务线 |
| 🔄 实时性 | 新增商品/内容 1 分钟内可被建议 |
| 💾 节省资源 | 减少对 Elasticsearch 的高频查询压力 |
| 🛡️ 高可用 | 缓存失效时可降级回 ES |
二、系统架构设计
+------------------+
| 用户输入 (前端) |
+--------+---------+
|
v
+------------------+
| API 网关 / 网关缓存 | ← 可选:CDN 或边缘缓存
+--------+---------+
|
v
+------------------+
| 搜索建议服务 |
| (Suggestion Service) |
+--------+---------+
|
+-----+------+-----------------+
| | |
v v v
+--------+ +-------------+ +------------------+
| Redis | | 本地缓存 | | Elasticsearch |
| (L1) | | (Caffeine) | | (兜底 & 预加载) |
+--------+ +-------------+ +------------------+
| | |
+------------+---------------+
|
v
+------------------+
| 数据更新系统 |
| (Kafka + Flink) |
+------------------+
✅ 采用 多级缓存架构:本地缓存 → Redis → ES
三、缓存层级设计
1. L1:本地缓存(Caffeine / Guava)
- 作用:极致低延迟,避免网络开销;
- 容量:小(10~100MB),存储最热前缀;
- TTL:60s;
- 数据结构:
Map<String prefix, List<Suggestion>>
Cache<String, List<Suggestion>> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.build();
✅ 适用于:
a,ap,iph等超高频前缀。
2. L2:Redis 集群(分布式缓存)
- 作用:共享缓存,支持多实例;
- 容量:大(GB 级),存储常见前缀;
- TTL:300s;
- 数据结构:
STRING:sug:prefix:iph → JSON 列表ZSET:sug:hot存储热门词用于预加载
SET sug:prefix:iph '["iPhone", "iPhone 15", "苹果手机"]' EX 300
3. L3:Elasticsearch(兜底 & 预加载源)
- 当缓存未命中时,回源查询 ES;
- 用于预加载热门前缀到 Redis。
"suggest": {
"text": "iph",
"completion": {
"field": "suggest_pinyin"
}
}
四、缓存 Key 设计
| 场景 | Key 模板 | 示例 |
|---|---|---|
| 普通前缀 | sug:prefix:{text} | sug:prefix:iph |
| 带业务类型 | sug:prefix:{type}:{text} | sug:prefix:product:iph |
| 带用户画像 | sug:prefix:{user_seg}:{text} | sug:prefix:vip:iph |
| 热门词集合 | sug:hot | ZADD sug:hot 100 "iph" |
✅ 支持多维度缓存隔离。
五、缓存更新策略
1. 写时更新(Write-through)
当新增商品或内容时,触发缓存更新:
void onProductCreated(Product product) {
List<String> inputs = generatePinyinAndChineseInputs(product.getTitle());
for (String input : inputs) {
String prefix = input.substring(0, Math.min(3, input.length())); // 前 3 字符
redisClient.zincrby("sug:hot", 1.0, prefix); // 提升热度
}
}
通过 Kafka 发送事件到建议服务。
2. 预加载(Pre-warming)
定时将热门前缀预加载到 Redis:
// 每 5 分钟执行
List<String> hotPrefixes = redis.zrevrange("sug:hot", 0, 99);
for (String prefix : hotPrefixes) {
List<Suggestion> suggestions = esSuggester.suggest(prefix);
redis.setex("sug:prefix:" + prefix, 300, serialize(suggestions));
}
使用 Flink 实时计算热门前缀。
3. 失效策略
| 场景 | 失效方式 |
|---|---|
| 商品下架 | 主动删除相关前缀缓存 |
| 商品改名 | 删除旧前缀,添加新前缀 |
| 缓存过期 | TTL 自动失效 |
| 批量更新 | 清空 Redis 缓存,触发预加载 |
六、查询流程设计
代码示例(Java)
public List<Suggestion> suggest(String prefix) {
// 1. 本地缓存
List<Suggestion> result = localCache.getIfPresent(prefix);
if (result != null) return result;
// 2. Redis
result = redis.get("sug:prefix:" + prefix);
if (result != null) {
localCache.put(prefix, result);
return result;
}
// 3. 回源 ES
result = esSuggester.suggest(prefix);
// 4. 写入缓存
if (result != null && !result.isEmpty()) {
redis.setex("sug:prefix:" + prefix, 300, result);
localCache.put(prefix, result);
}
return result;
}
七、降级与容错
| 故障 | 降级策略 |
|---|---|
| Redis 不可用 | 降级到本地缓存 + 直连 ES |
| ES 不可用 | 返回空列表或缓存数据(短 TTL) |
| 本地缓存 OOM | 自动驱逐,依赖 Redis |
| 预加载失败 | 继续服务,依赖实时查询 |
✅ 设置熔断器(如 Hystrix)防止雪崩。
八、性能优化建议 ✅
| 场景 | 建议 |
|---|---|
| 前缀截断 | 只缓存前 3~4 字符,减少 key 数量 |
| 批量查询 | 支持 mget 批量获取多个前缀 |
| 压缩存储 | Redis 中使用 Snappy/GZIP 压缩 |
| 异步写入 | 缓存更新异步化,避免阻塞主流程 |
| 热点探测 | 实时监控高频前缀,动态预加载 |
九、监控与告警
1. 关键指标
| 指标 | 说明 |
|---|---|
cache.hit_rate | 本地 + Redis 总命中率 |
latency.p99 | 建议查询延迟 |
redis.memory.used | Redis 内存使用 |
es.suggest.qps | 回源 ES 的 QPS(越低越好) |
2. 告警规则
- 缓存命中率 < 90%
- P99 延迟 > 50ms
- Redis 内存 > 85%
十、扩展建议
| 场景 | 建议方案 |
|---|---|
| 个性化建议 | 基于用户画像返回定制化建议 |
| A/B 测试 | 不同用户组返回不同排序 |
| 冷启动问题 | 初始加载运营配置的热门词 |
| 多语言支持 | 按语言分缓存 namespace |
| 边缘缓存 | CDN 缓存静态建议(如首页推荐) |
1433

被折叠的 条评论
为什么被折叠?



