目录
- 🌴一、实现"双路"删除
- 🌴二、关键代码
-
- [1. UserContext 工具类](#1. UserContext 工具类)
- [2. 删除逻辑:手动编排 RedisSearch 实现"知识同步销毁"](#2. 删除逻辑:手动编排 RedisSearch 实现“知识同步销毁”)
- 🌴三、踩坑记录
-
- [坑1:在这里插入代码片`1:Redis 插件缺失](#坑1:在这里插入代码片`1:Redis 插件缺失)
- 坑2:索引配置缺失导致的"数据幽灵"
- [坑3:URL 路径变量无法解析](#坑3:URL 路径变量无法解析)
- 坑4:路径匹配失败(静态资源混淆)
- 坑5:框架接口未实现
- [坑6:Java 类名冲突](#坑6:Java 类名冲突)
- [🌴四、 可复用的工程经验](#🌴四、 可复用的工程经验)
-
- [1. 向量数据库的"增删改查"同步模版](#1. 向量数据库的“增删改查”同步模版)
- [2. 可观测性是第一生产力](#2. 可观测性是第一生产力)
- [3. 框架解耦与底层兜底](#3. 框架解耦与底层兜底)
- [4. 线程上下文的"清洁工"习惯](#4. 线程上下文的“清洁工”习惯)
✨本篇内容:基于(7)实现数据持久化的情况下,当前端执行删除操作时,MySQL和向量数据库中的内容都能被删除。
🌴一、实现"双路"删除
当我在前端点击"删除"时,文章从MySQL移除,同时系统自动在 Redis 向量库中检索并销毁对应的所有分片。
🌴二、关键代码
1. UserContext 工具类
这是解决 user_id 为空及身份泄露的核心。
java
public class UserContext {
private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
public static void setUserId(Long userId) { USER_ID_HOLDER.set(userId); }
public static Long getUserId() { return USER_ID_HOLDER.get(); }
// 在 JwtFilter 的 finally 块中调用,防止线程池污染和内存泄漏
public static void clear() { USER_ID_HOLDER.remove(); }
}
2. 删除逻辑:手动编排 RedisSearch 实现"知识同步销毁"
由于 LangChain4j 适配器暂不支持 Redis 的批量删除,我们直接操作底层 Jedis 解决了 Not supported yet 报错。
java
@Transactional
public void deletePost(Long postId) {
// 1. MySQL 删除
postMapper.deleteById(postId);
// 2. 向量库手动同步清理 (使用原生 RedisSearch 指令)
try (UnifiedJedis jedis = new UnifiedJedis("redis://127.0.0.1:6379")) {
// 构建标签查询语法:@{articleId}:{23}
String queryText = String.format("@articleId:%d", postId);
// 在 article-index 索引中搜索所有关联片段
SearchResult result = unifiedJedis.ftSearch("article-index", queryText);
if (result.getTotalResults() > 0) {
System.out.println("--- [Debug] 发现 " + result.getTotalResults() + " 个向量片段 ---");
for (redis.clients.jedis.search.Document doc : result.getDocuments()) {
String redisKey = doc.getId(); // 获取如 article:uuid 这种 Key
unifiedJedis.del(redisKey);
System.out.println("已物理删除 Redis Key: " + redisKey);
}
}
}
}
🌴三、踩坑记录
坑1:在这里插入代码片`1:Redis 插件缺失
【报错信息】:
Factory method 'embeddingStore' threw exception; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR unknown command FT._LIST【真相】:连接的是普通版 Redis。向量库搜索(RediSearch)是 Redis 的增强模块。
【解决】:使用 Docker 部署
redis/redis-stack 镜像。
坑2:索引配置缺失导致的"数据幽灵"
【报错信息】:
[debug]-----result:SearchResult{Total results:0, Documents:[]}(即便数据已在 Redis 中,但检索不到)。【真相】:Redis 索引中未包含 articleId
字段。RedisSearch 只有在索引中声明的字段才可用于查询。
【解决】:在 AiConfig 中增加
.metadataKeys(Collections.singletonList("articleId")) 并执行 FLUSHALL
重建索引。
技巧:利用docker exec -it redis-stack redis-cli FT.INFO article-index命令来查看属性列表和类型。主义类型是因为在搜索queryText时数字和文本类型要用不同的语法。

坑3:URL 路径变量无法解析
【报错信息】:
Required URI template variable 'postId' for method parameter type Long is not present【真相】:@DeleteMapping("/{id}") 路径中的变量名与参数名 Long postId 不一致。
【解决】:统一变量名,或使用 @PathVariable("id") 显式指定映射。
坑4:路径匹配失败(静态资源混淆)
【报错信息】:
org.springframework.web.servlet.resource.NoResourceFoundException: No static resource posts/delete/22【真相】:Controller 未定义该路径。Spring Boot3 将无法匹配的 URL 默认转向静态资源目录。
【解决】:修正接口路径格式。
坑5:框架接口未实现
【报错信息】:
java.lang.UnsupportedOperationException: Not supported yet(在执行embeddingStore.removeAll(filter) 时)。【真相】:LangChain4j 的 Redis
适配器尚未完整实现批量删除接口。
【解决】:弃用框架 API,引入 UnifiedJedis 直接调用 Redis 原生 FT.SEARCH指令实现手动清理。
坑6:Java 类名冲突
【报错信息】:
java: 不兼容的类型: redis.clients.jedis.search.Document 无法转换为 dev.langchain4j.data.document.Document【真相】:两个框架都定义了 Document类,但语义完全不同。
【解决】:在代码中使用全限定名 redis.clients.jedis.search.Document 进行显式区分。
🌴四、 可复用的工程经验
1. 向量数据库的"增删改查"同步模版
经验:在双存储架构(MySQL + VectorDB)中,删除操作比写入更难。
模式:必须在写入时绑定文章唯一 ID 到元数据(Metadata),删除时采用 "先搜索元数据、再批量销毁 Key" 的原子操作。
2. 可观测性是第一生产力
经验:面对"黑盒"一样的 AI 检索,不要瞎猜。
模式:
后端日志:打印检索得分(Score)、召回片段内容、生成的原始 Prompt。
中间件工具:熟练使用 RedisInsight 查看数据的物理存储格式,利用 FT.INFO 检查索引配置。
3. 框架解耦与底层兜底
经验:框架(LangChain4j)是工具,但不是终点。
模式:当框架能力不足时,应具备直接操控底层原生协议(Jedis/RedisSearch)的能力。这种**"能上能下"**的技术栈掌握能力,是大厂面试官最看重的"硬实力"。
4. 线程上下文的"清洁工"习惯
经验:在处理跨层身份传递(ThreadLocal)时,必须严格遵守生命周期闭环。
模式:JwtFilter 的 finally 块中调用 UserContext.clear(),这是预防高并发环境下身份泄露和内存泄漏的"工业级金标准"。