拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

双层缓存架构:传统 Redis 精确匹配 + 向量近似检索的最佳协同路径

双层缓存架构:传统 Redis 精确匹配 + 向量近似检索的最佳协同路径

双层缓存架构:传统 Redis 精确匹配 + 向量近似检索的最佳协同路径

每次大促前做架构评审,调用外部大模型接口的账单总会让团队倒吸一口凉气。在日均数千万次咨询的客服场景中,大模型单次调用的成本哪怕只有几厘钱,放大到大促峰值也是一笔不小的开支。更致命的是响应延迟:大模型推理的 TTFT(Time to First Token)通常在 500ms 到 2s 之间,而前端用户对常见问题的心理预期是“秒回”。

仔细分析线上全量日志后会发现,大促期间超过 60% 的用户咨询其实都在询问极度集中的常规规则:“今年满减跨店怎么算?”、“活动优惠券能和红包叠加吗?”、“退货包运费险规则是什么?”。

如果我们对每一个问题都发起完整的 Embedding 计算和模型推理,既浪费算力,又拖慢响应。为了在降本、低延迟和高准确率之间找到平衡点,我们设计并落地了一套双层缓存架构:第一层采用经过文本规范化的传统 Redis 精确哈希匹配,第二层采用向量数据库近似检索(ANN),两层均未命中才穿透给大模型。


为什么不能单靠向量检索做语义缓存

现在很多开源框架(如 GPTCache)主打语义缓存(Semantic Cache),提倡将每一次用户的提问转化成 Embedding 向量,然后去向量数据库里检索余弦相似度(Cosine Similarity)。很多同学以为只要引入向量检索,就能包治百病,但在高并发生产场景下,纯向量检索有两大致命短板:

  1. 计算与检索延迟无法忽略:计算一段文本的 Embedding 向量,调用内网自建的小模型通常需要 10ms 到 30ms,再在向量索引中进行 HNSW 检索,耗时大约 5ms 到 15ms。整套流程下来,缓存命中的耗时也达到了 20ms 以上。如果 QPS 冲到数万,单纯为了算 Embedding 就会把向量推理服务压垮。
  2. 相似度阈值的边界效应:相似度阈值设得太高(如 0.95),许多意思完全相同但表达略有差异的问题无法命中;设得稍低(如 0.88),又经常出现致命误判。比如“我想要退货”与“我不想要退货”,在某些向量空间里的相似度高达 0.91,一旦误判返回,直接导致业务事故。

相反,传统 Redis 的 Key-Value 精确匹配,单次查询通常在 0.5ms 到 1ms 之间,内存吞吐量极高。对于高频点击的“猜你想问”快捷按钮或者完全相同的输入,精确匹配具有最高的性价比和确定性。


双层缓存的流转拓扑设计

我们将缓存拦截设计为三级漏斗:

[用户提问输入] │ ▼ [文本标准化管道] ─── (小写转换、去空格、去特殊标点、分词排序) │ ▼ 【L1 传统 Redis 精确缓存】 ──(命中: 0.8ms)──> [直接返回预存结果] │ (未命中) ▼ 【L2 向量语义近似检索】 ──(相似度 > 0.93)──> [返回高可信语义结果] │ (未命中) ▼ 【L3 穿透至大模型推理】 ───> [生成最新回答] │ └──── 异步回填 L1(精确Key) 与 L2(向量库)

核心实现细节与代码落地

1. 文本规范化清洗

用户输入的标点符号、全角半角、末尾空格极易导致传统哈希缓存失效。在生成 L1 缓存 Key 之前,必须经过确定性的清洗通道:

package com.yali.ai.cache; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TextNormalizer { /** * 过滤无关标点符号、统一转小写、合并连续空格 */ public static String normalize(String text) { if (text == null || text.isBlank()) { return ""; } // 去除首尾空格并转小写 String cleaned = text.trim().toLowerCase(); // 移除非文字/数字字符(仅保留中文、英文、数字) cleaned = cleaned.replaceAll("[\\p{Punct}\\p{Space}]+", ""); return cleaned; } /** * 计算 SHA-256 指纹作为 L1 Redis Key */ public static String computeDigest(String rawText) { String normalized = normalize(rawText); try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(normalized.getBytes(StandardCharsets.UTF_8)); StringBuilder hexString = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("SHA-256 不可用", e); } } }

2. 双层协同服务骨架

在业务入口层,我们通过统一的门面类协调 L1、L2 与 L3 的流转。未命中时,引入分布式互斥锁,防止高并发下同一冷门问题瞬间击穿到大模型。

package com.yali.ai.cache; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; @Service public class DualLayerAiCacheService { private final StringRedisTemplate redisTemplate; private final VectorSearchEngine vectorSearchEngine; private final EmbeddingModel embeddingModel; private final ChatClient chatClient; private final RedissonClient redissonClient; private static final String L1_PREFIX = "ai:cache:l1:"; private static final String LOCK_PREFIX = "ai:lock:query:"; private static final float SIMILARITY_THRESHOLD = 0.93f; public DualLayerAiCacheService(StringRedisTemplate redisTemplate, VectorSearchEngine vectorSearchEngine, EmbeddingModel embeddingModel, ChatClient chatClient, RedissonClient redissonClient) { this.redisTemplate = redisTemplate; this.vectorSearchEngine = vectorSearchEngine; this.embeddingModel = embeddingModel; this.chatClient = chatClient; this.redissonClient = redissonClient; } public String query(String rawPrompt) { // 1. L1 精确匹配查询 String digest = TextNormalizer.computeDigest(rawPrompt); String l1Key = L1_PREFIX + digest; String cachedAnswer = redisTemplate.opsForValue().get(l1Key); if (cachedAnswer != null) { return cachedAnswer; } // 2. L2 向量语义近似匹配 List<Float> promptVector = embeddingModel.embed(rawPrompt); SearchResult vectorMatch = vectorSearchEngine.findNearest(promptVector); if (vectorMatch != null && vectorMatch.getScore() >= SIMILARITY_THRESHOLD) { // 向量命中后,顺手回填 L1 缓存,加速后续完全相同的提问 redisTemplate.opsForValue().set(l1Key, vectorMatch.getContent(), Duration.ofHours(6)); return vectorMatch.getContent(); } // 3. L3 穿透至大模型:防击穿互斥锁 String lockKey = LOCK_PREFIX + digest; RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁,等待最多 3 秒,锁定 15 秒 if (lock.tryLock(3, 15, TimeUnit.SECONDS)) { try { // Double Check L1 缓存 String doubleCheck = redisTemplate.opsForValue().get(l1Key); if (doubleCheck != null) { return doubleCheck; } // 调用大模型推理 String answer = chatClient.prompt() .user(rawPrompt) .call() .content(); // 异步或同步双写回填缓存 redisTemplate.opsForValue().set(l1Key, answer, Duration.ofHours(12)); vectorSearchEngine.save(promptVector, rawPrompt, answer); return answer; } finally { lock.unlock(); } } else { // 未抢到锁的请求,稍作休眠重试读取 L1 Thread.sleep(200); String retryAnswer = redisTemplate.opsForValue().get(l1Key); return retryAnswer != null ? retryAnswer : "系统正在处理中,请稍候再试。"; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("请求被打断", e); } } }

生产运行的关键防护点

1. 缓存一致性与主动失效

很多团队在搞大模型缓存时最怕业务规则变动。例如晚上八点运营突然修改了“满 300 减 50”的凑单门槛,如果缓存里依然保留旧的解答,客服机器人就会给出错误的回答,引发投诉。

针对这种时效性强的信息,我们遵循两项原则:

  • 分类打标与 Tag 级联失效:在向向量库写入文档以及生成 L1 缓存时,将该问答所属的业务域(如PROMOTION_2026_DOUBLE11)作为 Tag 存入元数据。当运营后台更新规则发布广播消息时,通过消费 MQ 批量执行清空指定 Tag 的 L1 缓存与向量软删除。
  • 动态 TTL 与衰减因子:大促白天的缓存 TTL 不宜设置过长(推荐 4 到 8 小时),夜间业务相对静止时可适当延长,避免死缓存长期滞留。

2. 区分用户个性化状态

绝对不能将包含用户隐私(如“我昨天买的这双鞋发货了吗”)直接落入全局公共缓存池中。在进入缓存流水线前,需要通过轻量级正则或意图识别判断该问题是否包含“上下文绑定代词”(我、我的订单、这个地址)。一旦识别为个人状态查询,立刻绕过公共语义缓存,直接流转至业务 RPC 或带有专属上下文的 Agent 处理。


收益总结

通过这套“L1 精确指纹 + L2 向量近似”的双层漏斗架构:

  • 在日常巡检中,L1 精确匹配贡献了约 42% 的命中率,平均响应耗时 1.1ms;
  • L2 向量近似检索进一步捕获了 26% 的泛化表达问题,平均响应耗时 24ms;
  • 最终只有约 32% 的长尾复杂问题真正穿透到大模型生成环节。

不仅整体 P99 响应延迟从原本的 1800ms 压降到了 35ms 以内,大模型 Token 开销也直接砍掉了将近七成。在架构设计中,永远没有唯一的银弹,把最朴素的高性能组件与最前沿的 AI 技术拼接在合适的位置,才是资深工程师最有价值的基本功。

返回列表