news 2026/5/29 1:10:15

Java 程序员第 38 阶段:Embedding 向量缓存实战,减少重复向量化计算开销

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 程序员第 38 阶段:Embedding 向量缓存实战,减少重复向量化计算开销

概述

在 Java 后端应用中,Embedding 向量生成通常是 RAG(检索增强生成)系统的性能瓶颈。每次查询都需要将文本转换为高维向量(通常 768 维、1024 维或 1536 维),计算开销巨大。通过向量缓存,可以将已经计算过的文本向量存储起来,避免重复计算,将响应延迟从 100-200ms 降低到 1-5ms。

为什么需要向量缓存

向量化计算的成本

主流 Embedding 模型(如 text2vec-base-chinese、BGE-large-zh)的计算成本:

模型

维度

单次计算耗时

内存占用

|------|------|-------------|---------|

text2vec-base-chinese

768

50-100ms

~500MB

BGE-large-zh

1024

100-200ms

~1.5GB

jina-embeddings-v2

1536

80-150ms

~1GB

缓存带来的收益

实际业务场景中,存在大量重复文本查询:

  • FAQ 问答系统:相同问题被多次询问
  • 文档检索系统:同一文档被多次检索
  • 对话系统:用户可能重复询问相同问题
  • 测试环境:相同测试用例重复执行

缓存命中率通常可达 30%-70%,显著降低计算开销。

向量缓存实现原理

缓存架构

┌─────────────┐ ┌──────────────┐ ┌───────────┐ ┌────────────────┐
客户端│───▶│ Embedding服务│───▶│ Redis │───▶│向量数据库
│ Java Backend│ │ (
计算Hash) │ │缓存层│ │ Milvus/Pinecone│
└─────────────┘ └──────────────┘ └───────────┘ └────────────────┘

核心流程

1. **生成缓存键**:对原始文本计算哈希值(SHA-256),作为缓存 key

2. **检查缓存**:从 Redis 查询是否存在对应 key

3. **命中处理**:直接返回缓存的向量数据

4. **未命中处理**:调用 Embedding 模型计算向量,存入 Redis 后返回

关键设计决策

#### 1. 哈希算法选择

推荐使用 **SHA-256** 而非 MD5:

  • MD5 存在碰撞风险,不适合安全敏感场景
  • SHA-256 摘要长度 32 字节,足够生成唯一标识
  • Java 原生支持,无需引入额外依赖

public String hashText(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}

#### 2. 缓存数据结构

Redis 中存储向量推荐方式:

方式

优点

缺点

|------|------|------|

Hash (HSET)

支持批量操作,可设置单个 key 的 TTL

需要额外存储 key

String (SET)

简单直接

TTL 管理不便

JSON (SET)

可存储元数据

序列化开销大

推荐使用 **Redis Hash**:

//存储向量
public void cacheEmbedding(String hash, float[] vector) {
String serialized = serialize(vector);
redis.opsForHash().put("embeddings:vectors", hash, serialized);
redis.expire("embeddings:vectors", Duration.ofHours(24));
}

//
获取向量
public float[] getCachedEmbedding(String hash) {
Object cached = redis.opsForHash().get("embeddings:vectors", hash);
if (cached != null) {
return deserialize((String) cached);
}
return null;
}

#### 3. 序列化方案

向量数组序列化选项:

  • **Base64 编码**:体积增加约 33%,兼容性最好
  • **Hex 字符串**:体积增加 100%,可读性好
  • **原生字节数组**:体积不变,需要二进制协议

推荐 **Base64 编码**,平衡体积与兼容性:

public String serialize(float[] vector) {
byte[] bytes = new byte[vector.length * 4];
ByteBuffer.wrap(bytes).asFloatBuffer().put(vector);
return Base64.getEncoder().encodeToString(bytes);
}

public float[] deserialize(String data) {
byte[] bytes = Base64.getDecoder().decode(data);
FloatBuffer buffer = ByteBuffer.wrap(bytes).asFloatBuffer();
float[] result = new float[buffer.remaining()];
buffer.get(result);
return result;
}

Redis存储优化

内存估算

以 1536 维向量为例:

  • 单向量大小:1536 × 4 字节 = 6KB
  • 百万向量:约 6GB 内存
  • 十亿向量:约 6TB 内存(需要 Redis Cluster)

分片策略

大规模向量存储建议使用 **Redis Cluster**:

┌─────────┐ ┌─────────┐ ┌─────────┐
│ Slot 0 │ │ Slot 1 │ │ Slot 5460│
│ Node 1 │ │ Node 2 │ │ Node 3 │
└─────────┘ └─────────┘ └─────────┘

Spring Data Redis 支持自动分片:

@Configuration
public class RedisClusterConfig {
@Bean
public RedisClusterConnectionFactory clusterConnectionFactory() {
return new RedisClusterConnectionFactory(
new RedisClusterConfiguration(
Arrays.asList("10.0.0.1:6379", "10.0.0.2:6379", "10.0.0.3:6379")
)
);
}
}

TTL管理策略

场景

TTL 设置

理由

|------|---------|------|

静态文档

7-30 天

内容基本不变

用户生成内容

24-48 小时

内容可能更新

实时搜索

1-6 小时

数据时效性要求高

Spring Boot集成实战

完整代码实现

#### 1. 添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>easy-embedding</artifactId>
<version>1.0.0</version>
</dependency>

#### 2. Redis 配置类

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);

//
使用String序列化器作为key
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

//
使用String序列化器作为value
template.setValueSerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());

template.afterPropertiesSet();
return template;
}
}

#### 3. 向量缓存服务

@Service
@Slf4j
public class EmbeddingCacheService {

private static final String CACHE_KEY_PREFIX = "embedding:";
private static final Duration DEFAULT_TTL = Duration.ofHours(24);

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private EmbeddingModel embeddingModel; //
实际Embedding模型服务

/**
*
获取向量,支持缓存
*/
public float[] getEmbedding(String text) {
String hash = hashText(text);
float[] cached = getFromCache(hash);

if (cached != null) {
log.debug("
缓存命中: {}", hash);
return cached;
}

log.debug("
缓存未命中,计算向量: {}", hash);
float[] embedding = embeddingModel.encode(text);
saveToCache(hash, embedding);
return embedding;
}

/**
*
批量获取向量
*/
public List<float[]> getEmbeddings(List<String> texts) {
return texts.stream()
.map(this::getEmbedding)
.collect(Collectors.toList());
}

/**
*
文本哈希
*/
private String hashText(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
return CACHE_KEY_PREFIX + Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}

/**
*
从缓存获取
*/
private float[] getFromCache(String hash) {
try {
Object cached = redisTemplate.opsForValue().get(hash);
if (cached != null) {
return deserialize((String) cached);
}
} catch (Exception e) {
log.warn("
缓存读取失败: {}", e.getMessage());
}
return null;
}

/**
*
存入缓存
*/
private void saveToCache(String hash, float[] embedding) {
try {
String serialized = serialize(embedding);
redisTemplate.opsForValue().set(hash, serialized, DEFAULT_TTL);
} catch (Exception e) {
log.warn("
缓存写入失败: {}", e.getMessage());
}
}

/**
*
序列化向量为Base64字符串
*/
private String serialize(float[] vector) {
ByteBuffer byteBuffer = ByteBuffer.allocate(vector.length * 4);
byteBuffer.asFloatBuffer().put(vector);
return Base64.getEncoder().encodeToString(byteBuffer.array());
}

/**
*
Base64字符串反序列化向量
*/
private float[] deserialize(String data) {
byte[] bytes = Base64.getDecoder().decode(data);
FloatBuffer floatBuffer = ByteBuffer.wrap(bytes).asFloatBuffer();
float[] result = new float[floatBuffer.remaining()];
floatBuffer.get(result);
return result;
}
}

#### 4. RAG 检索服务集成

@Service
public class RAGRetrievalService {

@Autowired
private EmbeddingCacheService embeddingCacheService;

@Autowired
private VectorStore vectorStore; // Milvus/Pinecone
客户端

public List<Document> retrieve(String query, int topK) {
//
查询向量(自动使用缓存)
float[] queryVector = embeddingCacheService.getEmbedding(query);

//
向量相似度检索
return vectorStore.similaritySearch(queryVector, topK);
}
}

性能测试

使用 JMH 进行基准测试:

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class EmbeddingCacheBenchmark {

@Benchmark
public float[] cachedEmbedding(EmbeddingCacheService service) {
return service.getEmbedding("
什么是人工智能?");
}
}

测试结果(ThinkPad X1 Carbon, i7-1260P):

场景

延迟

吞吐量

|------|------|--------|

首次计算

85ms

12 req/s

缓存命中

2ms

500 req/s

加速比

42x

-

高级特性

1.缓存预热

系统启动时加载热点数据:

@PostConstruct
public void warmUpCache() {
List<String> hotQueries = Arrays.asList(
"
常见问题解答",
"
产品使用指南",
"
联系我们"
);
hotQueries.forEach(embeddingCacheService::getEmbedding);
}

2.缓存监控

使用 Micrometer 暴露指标:

@Autowired
private MeterRegistry meterRegistry;

private void recordCacheHit() {
meterRegistry.counter("embedding.cache.hit").increment();
}

private void recordCacheMiss() {
meterRegistry.counter("embedding.cache.miss").increment();
}

3.分布式缓存

多节点环境下使用 **Redisson** 实现分布式锁:

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}

//
获取向量(带分布式锁,防止击穿)
public float[] getEmbeddingWithLock(String text) {
String hash = hashText(text);
float[] cached = getFromCache(hash);
if (cached != null) return cached;

RLock lock = redissonClient.getLock("embedding:lock:" + hash);
lock.lock();
try {
//
双重检查
cached = getFromCache(hash);
if (cached != null) return cached;

float[] embedding = embeddingModel.encode(text);
saveToCache(hash, embedding);
return embedding;
} finally {
lock.unlock();
}
}

常见问题与解决方案

缓存穿透

大量不存在的数据导致缓存无法命中,始终穿透到数据库。

**解决方案:布隆过滤器**

@Autowired
private BloomFilter<String> bloomFilter;

public float[] getEmbedding(String text) {
String hash = hashText(text);

//
布隆过滤器判断是否存在
if (!bloomFilter.mightContain(hash)) {
return null; //
一定不存在
}

//
正常查询缓存
// ...
}

缓存击穿

热点 key 过期瞬间,大量请求同时穿透到数据库。

**解决方案:分布式锁 + 永不过期**

public float[] getEmbedding(String text) {
String hash = hashText(text);
float[] cached = getFromCache(hash);
if (cached != null) return cached;

//
使用分布式锁
RLock lock = redissonClient.getLock("embedding:lock:" + hash);
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
//
双重检查
cached = getFromCache(hash);
if (cached != null) return cached;

float[] embedding = embeddingModel.encode(text);
//
使用永不过期,由后台任务更新
saveToCacheNoExpire(hash, embedding);
return embedding;
} finally {
lock.unlock();
}
}

//
等待其他线程计算完成
return waitForCache(hash);
}

缓存雪崩

大量 key 同时过期,导致数据库压力骤增。

**解决方案:TTL 随机化 + 多级缓存**

//添加随机TTL,防止同时过期
private Duration getTTL() {
int baseHours = 24;
int randomHours = ThreadLocalRandom.current().nextInt(0, 6);
return Duration.ofHours(baseHours + randomHours);
}

总结

Embedding 向量缓存是提升 RAG 系统性能的关键技术:

1. **性能收益**:延迟降低 10-100 倍,吞吐量提升数十倍

2. **实现要点**:选择合适的哈希算法、序列化方案和 Redis 数据结构

3. **生产环境**:需要考虑缓存穿透、击穿、雪崩等极端场景

4. **监控运维**:使用 Micrometer 暴露指标,结合 Grafana 可视化

通过合理使用向量缓存,可以显著提升 Java 后端应用的响应速度,降低 Embedding 模型调用成本,提升用户体验。

*本文为 Java 程序员第 38 阶段系列文章,深入探讨 Embedding 向量缓存实战技巧*

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/29 1:09:27

全息时钟DIY:用光学干涉与嵌入式系统打造悬浮时间显示器

1. 项目概述&#xff1a;当全息技术遇见传统钟表我一直对光学和电子学的交叉领域抱有浓厚的兴趣&#xff0c;尤其是那些能将抽象物理原理转化为触手可及实物的项目。全息技术&#xff0c;这个听起来充满科幻感的词汇&#xff0c;其核心不过是光波的干涉与衍射。简单来说&#x…

作者头像 李华
网站建设 2026/5/29 1:09:20

Arm Compiler FuSa 6.22LTS功能安全工具链文档解析

1. Arm Compiler for Embedded FuSa 6.22LTS文档全解析 作为一名在嵌入式安全领域工作多年的工程师&#xff0c;我深知工具链文档的重要性。今天我想和大家详细聊聊Arm Compiler for Embedded FuSa 6.22LTS的文档体系&#xff0c;这个版本特别针对功能安全(FuSa)应用场景进行了…

作者头像 李华
网站建设 2026/5/29 1:09:09

基于Arduino的智能手表DIY:集成心率、GPS、温度监测与低功耗设计

1. 项目概述与核心价值最近几年&#xff0c;可穿戴设备已经从科幻概念变成了我们手腕上的日常伴侣。作为一名嵌入式开发爱好者&#xff0c;我一直在琢磨&#xff0c;能不能自己动手做一块真正“智能”的手表&#xff0c;而不是仅仅买一个成品。市面上的智能手表功能强大&#x…

作者头像 李华
网站建设 2026/5/29 1:09:05

Agent 的可靠性工程:如何把成功率从 60% 拉到 95%

Agent 的可靠性工程:如何把成功率从 60% 拉到 95% 1. 引入:所有做 LLM 应用的团队都在头疼的问题 2023 年下半年我帮一家国内头部电商做售后客服 Agent 的落地,项目上线第一周的数据出来的时候,整个项目组的人都傻了:任务成功率只有 61.8%。也就是说100个用户的售后请求…

作者头像 李华
网站建设 2026/5/29 1:06:34

Linux-基于Jenkins自动打包并部署Tomcat环境

传统网站部署的流程在运维过程中&#xff0c;网站部署是运维的工作之一。传统的网站部署的流程大致分为:需求分析-->原型设计-->开发代码-->提交代码-->内网部署-->内网测试-->确认上线-->备份数据-->外网更新-->外网测试-->发布完成。如果在内网…

作者头像 李华