Spring AI 官方 starter 竟有坑!ChatMemory 持久化踩坑实录
Spring AI 官方 starter 竟有坑!ChatMemory 持久化踩坑实录
这是 Spring AI 系列第 3 篇。还没看过前两篇的:
一、内存版的致命缺陷
上一篇文章我实现了 Spring AI 的多轮对话记忆,用的是内存版 MessageWindowChatMemory。本地测试跑得飞起,AI 记住我说过的每句话。
但当我把应用重启后——
你叫什么名字?
抱歉,您还没有告诉我您的名字。
我:???我上一秒才说的啊。
再一查代码,瞬间明白了:内存版的 ChatMemory 存在 JVM 内存里,应用一重启,内存清空,所有对话记录全丢了。
问题有三个:
- 重启丢失:应用重启 / 容器重启,内存数据清空
- 多实例不同步:两个实例跑在两台机器上,实例 A 的对话记录实例 B 看不到
- 数据无审计:无法查询"某用户上周聊了什么",内存里的数据转瞬即逝
对于生产环境,这 3 个问题都是不可接受的。
二、官方的 JDBC 持久化方案
Spring AI 官方提供了 spring-ai-starter-model-chat-memory-repository-jdbc,两个关键组件:
- JdbcChatMemoryRepository:负责读写数据库
- 自动建表脚本:启动时自动创建
SPRING_AI_CHAT_MEMORY表
理论上,只需要:
Step 1:加依赖
<!-- ChatMemory JDBC 持久化 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
Step 2:改 ChatMemoryConfig
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20)
.build();
}
}
Step 3:配 datasource
spring:
datasource:
url: jdbc:mysql://localhost:3306/hello_ai
username: ${MYSQL_USER:root}
password: ${MYSQL_PASSWORD:root}
driver-class-name: com.mysql.cj.jdbc.Driver
然后启动应用,应该自动建表 + 一切正常。
三、踩坑:官方 jar 竟然没有 MySQL 建表脚本 🔥
我按上述配置启动,结果报错:
No schema scripts found at location 'classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-mysql.sql'
第一反应:yml 配置错了?再检查一遍 datasource.url,正确。MySQL 连接正常,还能打印出 HikariPool Start completed。
第二反应:依赖版本错了?再看一遍 pom.xml,spring-ai.version 是 1.0.0,正确。
最后决定:不猜了,直接去 jar 里看真相。用命令解压 spring-ai-model-chat-memory-repository-jdbc-1.0.0.jar,列出里面所有的 .sql 文件:
jar tf spring-ai-model-chat-memory-repository-jdbc-1.0.0.jar | grep sql
结果震惊了:
org/springframework/ai/chat/memory/repository/jdbc/schema-postgresql.sql ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-sqlserver.sql ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-hsqldb.sql ✓
org/springframework/ai/chat/memory/repository/jdbc/schema-mariadb.sql ✓
没有 schema-mysql.sql!!!
jar 里有 MysqlChatMemoryRepositoryDialect.class(说明支持 MySQL),但建表脚本漏掉了。这是 Spring AI 1.0.0 的一个官方缺陷——估计是打包时漏了。
四、解决方案:自己提供 schema-mysql.sql
既然官方没提供,就自己写一个。参考 jar 里其他数据库的脚本,仿照 schema-mariadb.sql(MySQL 语法最接近),写了 schema-mysql.sql:
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
conversation_id VARCHAR(36) NOT NULL,
content TEXT NOT NULL,
type VARCHAR(10) NOT NULL,
`timestamp` TIMESTAMP NOT NULL,
CONSTRAINT SPRING_AI_CHAT_MEMORY_TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
);
表结构 4 个字段:
conversation_id:会话 IDcontent:消息内容type:消息类型(USER/ASSISTANT/SYSTEM/TOOL)timestamp:时间戳
把脚本放在 src/main/resources/schema-mysql.sql,然后改 application.yml:
spring:
ai:
chat:
memory:
repository:
jdbc:
# 关掉 starter 自带的建表初始化器(它找不到 schema-mysql.sql)
initialize-schema: never
# 用 Spring Boot 的 sql.init 指定我们的脚本
sql:
init:
mode: always
schema-locations: classpath:schema-mysql.sql
配置逻辑:
initialize-schema: never:关掉 starter 的自动建表sql.init.mode: always:让 Spring Boot 在启动时执行我们的脚本schema-locations:指定脚本位置
重启应用,控制台打印:
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (...)
启动成功!
五、效果验证:重启后记忆还在
启动后,跑这套测试(关键在第 2 步手动重启):
# 第 1 步:发消息
http://localhost:8080/ai/chat?sessionId=persist01&message=我叫张三,今年28岁
# 第 2 步:【手动重启应用】(Ctrl+F5)
# 第 3 步:重启后,用同一个 sessionId 再问
http://localhost:8080/ai/chat?sessionId=persist01&message=我叫什么名字?
结果:AI 回答**“您叫张三”**!
对比内存版(重启后失忆),这次重启后记忆还在,说明数据确实存进了 MySQL。
用 Navicat 查数据库,也能看到记录:
SELECT * FROM hello_ai.SPRING_AI_CHAT_MEMORY;
这就是持久化的铁证:数据落地,重启不丢。
六、生产环境最佳实践
6.1 索引优化
脚本里我没加索引,因为应用层已经用 maxMessages=20 限制了单会话数据量。但如果你数据量起来了,可以手动加索引:
CREATE INDEX SPRING_AI_CHAT_MEMORY_CID_TS_IDX
ON SPRING_AI_CHAT_MEMORY (conversation_id, `timestamp`);
6.2 数据清理
对话表会无限增长,建议定期清理:
@Scheduled(cron = "0 0 2 * * ?")
public void cleanOldMessages() {
jdbcTemplate.update(
"DELETE FROM SPRING_AI_CHAT_MEMORY WHERE `timestamp` < DATE_SUB(NOW(), INTERVAL 30 DAY)"
);
}
6.3 敏感信息脱敏
如果对话内容涉及用户隐私,存数据库前脱敏(需要自定义 Advisor):
.advisors(a -> a
.param(ChatMemory.CONVERSATION_ID, sessionId)
.beforeReply((context, request) -> {
String content = request.getUserText().replaceAll("\\d{15,}", "***");
// 修改 request 的 user text
})
)
七、我学到了什么
- 内存版只能开发用,生产必须持久化——重启丢失是致命缺陷
- 官方库也有坑——Spring AI 1.0 的 MySQL schema 缺失就是例子
- 排查问题时去 jar 里看真相——不靠猜,直接解压 jar 看有什么文件
- Spring Boot 的
sql.init比自动配置更可控——当 starter 不满足需求时,用底层的sql.init绕过
最大的体会:所谓"官方 starter",本质上是一堆约定好的配置 + 脚本。当它不满足你的需求时,跳出来自己配,往往更清晰。
八、下一篇预告
这篇用了 MySQL 持久化,解决了"重启丢失"问题。但还有一个性能问题没解决:
当对话记录达到几千条、上万条时,
SELECT * FROM chat_memory WHERE conversation_id = ?会越来越慢。
下一篇我会写:
《ChatMemory 性能优化:从单表查询到 Redis 缓存》
包括:
- 为什么单表查询会慢
- Redis 持久化配置
- MySQL + Redis 混合策略
- 性能测试:从 1W 条到 100W 条的查询耗时对比
这是把 demo 推向高并发的必经之路。
写在最后
我是一名8 年 Java 后端,正在转型 AI 应用开发。Spring AI 系列会持续更新,从 hello world 到 RAG 到 Agent,一路踩坑一路写。
如果你也在转型 AI,关注我,一起走。有问题评论区聊,我会逐条回复。
如果这篇文章帮到了你,点个赞就是对我最大的鼓励 ❤️
更多推荐




所有评论(0)