Spring AI 使用 MySQL 持久化 ChatMemory 实战
Spring AI 的 ChatMemory 组件默认使用内存存储,存在应用重启数据丢失及无法跨实例共享的问题。解决方案是将 ChatMemory 持久化至 MySQL 数据库。主要内容包括依赖引入、配置详解、代码改造(仅需修改 Bean)、自动配置机制、表结构分析、工作流程及生产环境最佳实践。通过切换 JdbcChatMemoryRepository 实现平滑迁移,支持分布式共享与数据分析,满足合规性要求。

Spring AI 的 ChatMemory 组件默认使用内存存储,存在应用重启数据丢失及无法跨实例共享的问题。解决方案是将 ChatMemory 持久化至 MySQL 数据库。主要内容包括依赖引入、配置详解、代码改造(仅需修改 Bean)、自动配置机制、表结构分析、工作流程及生产环境最佳实践。通过切换 JdbcChatMemoryRepository 实现平滑迁移,支持分布式共享与数据分析,满足合规性要求。

在构建 AI 对话应用时,对话历史(Chat Memory)的管理至关重要。Spring AI 提供的 ChatMemory 组件能够帮助开发者轻松实现多轮对话能力,让大模型能够记住之前的对话内容,进而提供更连贯、更具上下文感知的回复。
然而,默认的内存存储(InMemoryChatMemoryRepository)存在明显的局限性:应用重启时数据丢失,无法跨实例共享,难以应对生产环境需求。这正是本篇文章的核心议题——如何将 ChatMemory 持久化到 MySQL 数据库中。
通过这篇文章,你将学会:
让我们先回顾一下之前使用的 InMemory 存储方式:
@Bean
public ChatMemory chatMemory() {
InMemoryChatMemoryRepository inMemoryChatMemoryRepository = new InMemoryChatMemoryRepository();
return MessageWindowChatMemory.builder().chatMemoryRepository(inMemoryChatMemoryRepository).build();
}
看似简洁的代码,却隐藏着几个严重问题:
问题 1:应用重启数据丢失
问题 2:无法跨实例共享
问题 3:内存压力大
问题 4:无法进行数据分析
问题 5:缺乏审计能力
采用 MySQL 存储后,这些问题迎刃而解:
在 pom.xml 中添加以下依赖:
<!-- Spring AI JDBC ChatMemory Repository -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- MySQL JDBC 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
依赖说明:
spring-ai-starter-model-chat-memory-repository-jdbc:Spring AI 提供的 JDBC 实现,包含自动配置和表初始化逻辑mysql-connector-java 8.0.33:MySQL Java 驱动程序,确保与数据库的连接这两个依赖是实现 JDBC ChatMemory 的最小必要配置。
在 application.yml 中配置数据源连接:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/my_db?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowMultiQueries=true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: artisan123456
连接字符串参数解释:
| 参数 | 说明 |
|---|---|
useUnicode=true | 使用 Unicode 编码,确保中文正常存储 |
characterEncoding=utf-8 | 字符集为 UTF-8,支持中文和其他 Unicode 字符 |
zeroDateTimeBehavior=convertToNull | MySQL 中 0000-00-00 转换为 null,避免异常 |
transformedBitIsBoolean=true | MySQL BIT 类型映射为 Java boolean |
allowMultiQueries=true | 允许多条 SQL 语句一起执行 |
allowPublicKeyRetrieval=true | 允许使用公钥检索进行认证 |
useSSL=false | 不使用 SSL 连接(开发环境) |
serverTimezone=Asia/Shanghai | 设置时区为上海,避免时间错位 |
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
配置参数:
| 参数 | 可选值 | 说明 |
|---|---|---|
initialize-schema | always / never / create-if-missing | always:每次启动时重建表结构;never:从不初始化;create-if-missing:表不存在时创建 |
推荐配置策略:
always,每次启动都重新初始化,确保表结构最新create-if-missing,只在首次运行时创建never,由 DBA 负责初始化和维护表结构spring:
# 数据源配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/my_db?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&allowMultiQueries=true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: artisan123456
# AI 配置
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${DASHSCOPE_API_KEY}
chat:
options:
model: qwen3-max
# ChatMemory JDBC 配置
memory:
repository:
jdbc:
initialize-schema: always
server:
port: 8081
Spring AI 的优秀架构设计体现在这里:从 InMemory 切换到 JDBC,只需修改一个 Bean 定义,无需改动任何业务代码。
Before(使用 InMemory):
@Bean
public ChatMemory chatMemory() {
InMemoryChatMemoryRepository inMemoryChatMemoryRepository = new InMemoryChatMemoryRepository();
return MessageWindowChatMemory.builder().chatMemoryRepository(inMemoryChatMemoryRepository).build();
}
After(使用 JDBC):
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).build();
}
只改动两处:
InMemoryChatMemoryRepository inMemoryChatMemoryRepository = new InMemoryChatMemoryRepository(); 这一行JdbcChatMemoryRepository chatMemoryRepository,让 Spring 自动注入这正是依赖注入和接口抽象的威力——ChatMemoryRepository 是抽象接口,具体实现可以自由切换,业务代码完全不受影响。
这体现了 SOLID 设计原则中的依赖倒置原则(DIP):高层模块(业务层)依赖于抽象接口(ChatMemoryRepository),而不是依赖于具体实现。这样做的好处是:
业务层代码完全无需改动。例如,在 Controller 中使用 ChatMemory:
@Autowired
private ChatMemory chatMemory;
@GetMapping("/memory")
public String memory(@RequestParam("chatId") String chatId, @RequestParam("question") String question) {
return chatClient
.prompt()
.advisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.advisors(advisorSpec -> advisorSpec.params(Map.of(ChatMemory.CONVERSATION_ID, chatId)))
.user(question)
.call()
.content();
}
这段代码在 InMemory 和 JDBC 之间切换时,一行都不需要改。这就是良好的抽象设计的价值所在。
当从 InMemory 迁移到 JDBC 时:
第一次启动时的行为变化:
initialize-schema: always 时会自动创建表结构对话数据的处理:
性能特性:
故障模式:
当我们在 pom.xml 中添加 spring-ai-starter-model-chat-memory-repository-jdbc 依赖后,Spring Boot 的自动配置机制会自动发现并应用相关配置。
这个过程包含几个关键步骤:
第一步:classpath 扫描
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件spring-ai-starter-model-chat-memory-repository-jdbc 包含该文件,指向 JDBC ChatMemory 的自动配置类第二步:自动配置类加载
JdbcChatMemoryRepositoryAutoConfiguration)被 Spring 加载第三步:条件判断
@Configuration
@ConditionalOnClass(JdbcChatMemoryRepository.class)
@ConditionalOnProperty(
prefix = "spring.ai.chat.memory.repository.jdbc",
name = "initialize-schema",
havingValue = "always|create-if-missing|never"
)
public class JdbcChatMemoryRepositoryAutoConfiguration {
// ...
}
@ConditionalOnClass:classpath 中必须存在 JdbcChatMemoryRepository 类@ConditionalOnProperty:application.yml 中必须配置 spring.ai.chat.memory.repository.jdbc.initialize-schema第四步:Bean 创建 当所有条件满足时,自动配置类创建以下 Bean:
@Bean
@ConditionalOnMissingBean
public JdbcChatMemoryRepository chatMemoryRepository(JdbcOperations jdbcOperations, JdbcChatMemoryRepositoryProperties properties) {
return new JdbcChatMemoryRepository(jdbcOperations, properties);
}
JdbcOperations:Spring 提供的 JDBC 操作工具JdbcChatMemoryRepositoryProperties:从配置文件读取的属性第五步:依赖注入 我们定义的 Bean 方法:
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository) {
// ...
}
Spring 检测到参数 JdbcChatMemoryRepository,自动注入刚才创建的 Bean。
这套机制的优雅之处在于:
@Configuration 类或 @Bean 方法来创建 JdbcChatMemoryRepository当应用启动且 initialize-schema: always 时,Spring AI 会自动执行初始化脚本。让我们看看生成的表结构:
CREATE TABLE message_store (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversation_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL,
content LONGTEXT NOT NULL,
timestamp BIGINT NOT NULL,
INDEX idx_conversation_id (conversation_id),
INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
这个表结构遵循了数据库设计最佳实践:
| 字段名 | 类型 | 说明 | 索引 |
|---|---|---|---|
id | BIGINT AUTO_INCREMENT PRIMARY KEY | 消息全局唯一标识,自增主键 | 是 |
conversation_id | VARCHAR(255) NOT NULL | 对话会话 ID,用于隔离不同用户/会话的对话记录 | 是 |
message_type | VARCHAR(50) NOT NULL | 消息类型:USER(用户消息)或 ASSISTANT(模型回复) | 否 |
content | LONGTEXT NOT NULL | 消息内容,支持长文本 | 否 |
timestamp | BIGINT NOT NULL | 消息时间戳,毫秒级精度 | 是 |
字段设计的考虑:
idx_conversation_id 索引的作用:
idx_timestamp 索引的作用:
为什么 content 不建立索引:
// 数据流动过程
用户消息 → MessageWindowChatMemory → JdbcChatMemoryRepository
↓ INSERT INTO message_store (conversation_id, message_type, content, timestamp) VALUES (?,?,?,?)
// 读取过程
按 conversation_id 查询 → SELECT * FROM message_store WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?
Spring AI 的 JdbcChatMemoryRepository 会自动处理以下逻辑:
假设用户 chatId 为"user123"进行对话,表中的数据可能是这样的:
| id | conversation_id | message_type | content | timestamp |
|---|---|---|---|---|
| 1 | user123 | USER | 你好,请介绍一下 Spring AI | 1700000000000 |
| 2 | user123 | ASSISTANT | 你好!Spring AI 是 Spring 框架的 AI 扩展…(完整回复) | 1700000001000 |
| 3 | user123 | USER | 如何使用 ChatMemory 实现多轮对话? | 1700000002000 |
| 4 | user123 | ASSISTANT | 在 Spring AI 中,ChatMemory 用于存储…(完整回复) | 1700000003000 |
通过按 conversation_id 分组,可以轻松实现多个独立的对话会话。
性能考虑:
用户请求 → ChatClient.prompt()
↓ MessageChatMemoryAdvisor 检测到使用 ChatMemory
↓ 加载 conversation_id 对应的历史消息
↓ 组装系统消息 + 历史消息 + 用户新消息
↓ 调用大模型获取回复
↓ 回复内容返回给用户
↓ MessageChatMemoryAdvisor 保存用户消息和 AI 回复到数据库
↓ JdbcChatMemoryRepository.add() 执行 INSERT 操作
详细步骤分析:
如下时序图描绘了 Spring AI 结合 JdbcChatMemoryRepository 实现的 持久化记忆管理 全链路。它展示了消息如何从内存流转到关系型数据库(如 MySQL/PostgreSQL)。
大模型 (Qwen) 数据库 (JDBC) JdbcChatMemoryRepository MessageChatMemoryAdvisor ChatClient
1. 对话开始
2. 读取持久化记忆
3. 上下文增强与推理
4. 响应分发
5. 记忆持久化 (Post-Process)
记忆保存完成
prompt(question)
1 getMessages(conversationId)
2 SELECT * FROM chat_memory WHERE ...
3 返回历史消息记录
4 转换为 List<Message>
5 组装 [System + History + User]
6 发送完整 Context
7 返回 AI 回复内容
8 返回结果给用户
9 add(conversationId, userMsg)
10 add(conversationId, aiMsg)
11 INSERT INTO chat_memory ... (用户消息)
12 INSERT INTO chat_memory ... (AI 回复)
13
JdbcChatMemoryRepository 是 Spring AI 提供的一个标准实现。它利用 JdbcTemplate 将对话对象序列化为数据库行。默认情况下,它通常包含 chat_id、message_type(USER/ASSISTANT)和 content 等字段。ChatClient 被触发时,Advisor 才会去数据库捞取历史。这保证了即使应用重启,用户的对话上下文依然存在。conversation_id(通常由前端传入或从 Session 获取),系统可以同时处理成千上万个并发用户的独立记忆,互不干扰。在高性能场景下,频繁的 SELECT 和 INSERT 可能会成为瓶颈。你是否考虑过:
add() 操作放入异步线程池,不阻塞用户的响应时间。应用启动时或新会话开始
↓ MessageChatMemoryAdvisor 收到请求
↓ 调用 ChatMemory.getMessages(conversationId)
↓ JdbcChatMemoryRepository.query()
↓ 执行 SELECT * FROM message_store WHERE conversation_id = ? ORDER BY timestamp
↓ 将结果转换为 Message 对象列表
↓ MessageWindowChatMemory 根据滑动窗口策略筛选消息
↓ 返回最近 N 条消息供模型使用
关键点说明:
下面的时序图展示了 Spring AI 中持久化存储与滑动窗口策略相结合的精细化记忆加载流程。它解释了系统如何在海量历史数据中,既保证'记得住'(JDBC 持久化),又保证'不超限'(滑动窗口筛选)。
数据库 (MySQL/PG) JdbcChatMemoryRepository MessageWindowChatMemory (装饰器) MessageChatMemoryAdvisor
1. 触发记忆检索
2. 全量/增量从库读取
3. 执行滑动窗口策略 (Memory Pruning)
丢弃过旧的 Context,防止 Token 溢出
4. 返回精简后的上下文
5. 注入 Prompt 并发送给模型
getMessages(conversationId)
1 getMessages(conversationId)
2 SELECT * FROM message_store WHERE id = ? ORDER BY ts
3 返回所有历史行 (ResultSet)
4 转换为 List<Message> (全量历史)
5 筛选最近 N 条消息 (e.g., Last 10)
6 返回 List<Message> (Size <= N)
7
JdbcChatMemoryRepository:只负责'搬运'。它不关心消息有多少,只负责把数据库里的数据变成 Java 对象。**MessageWindowChatMemory**:负责'剪裁'。它作为包装层,根据配置的 capacity(容量)对原始数据进行切片。LLM 的上下文窗口(Context Window)是有限的(如 128k tokens)。如果不做筛选:
在第 5 步的 SELECT 语句中,如果对话历史达到数万条,全量加载到内存再进行 Window 筛选会变得非常缓慢。
优化建议:在生产环境中,通常会直接在 SQL 层面通过
LIMIT和ORDER BY DESC来实现物理层面的窗口筛选,例如:
SELECT * FROM message_store WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT 20
这种结构非常稳健。当对话非常长,但又不能简单丢弃旧信息时,如何通过 Vector Database (RAG) 来实现'语义搜索式'的记忆检索,而不是简单的'最近 N 条'
// 1. 用户调用 API
chatClient.prompt()
.advisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user123"))
.user(question)
.call()
// 2. MessageChatMemoryAdvisor 执行
// 2.1 读取历史消息
List<Message> messages = chatMemory.getMessages("user123");
// 2.2 JdbcChatMemoryRepository 查询数据库
// 执行 SQL: SELECT * FROM message_store WHERE conversation_id = 'user123' ORDER BY timestamp DESC LIMIT 10
// 2.3 组装完整的消息列表
messages.add(newUserMessage(question));
// 3. 大模型处理
// 调用 OpenAI API,传入消息列表
{
"model": "qwen3-max",
"messages": [
{"role": "user", "content": "...上一轮问题..."},
{"role": "assistant", "content": "...上一轮回复..."},
{"role": "user", "content": "...新问题..."}
]
}
// 4. MessageChatMemoryAdvisor 保存回复
chatMemory.add("user123", response);
// 4.1 JdbcChatMemoryRepository 插入数据库
// 执行 SQL: INSERT INTO message_store (conversation_id, message_type, content, timestamp) VALUES ('user123', 'ASSISTANT', '...AI 回复...', 1700000001000)
在高并发环境中,多个用户同时发送消息时,Spring AI 的处理方式:
// 用户 A 和用户 B 同时发送消息
// 线程 1:处理用户 A 的消息
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "userA"))
.user("问题 A")
.call()
// 通过 conversation_id 隔离
// 线程 2:处理用户 B 的消息
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "userB"))
.user("问题 B")
.call()
// 完全独立,不相互影响
数据库层面:
MessageWindowChatMemory 并非简单地返回所有历史消息,而是通过滑动窗口策略来控制消息数量:
MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.windowSize(10) // 默认值,保留最近 10 条消息
.build();
这个机制有两个作用:
通过 conversation_id 实现会话隔离:
// 用户 A 和用户 B 的对话完全隔离
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "userA"))
.user("你好")
.call();
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "userB"))
.user("你好")
.call();
两个用户的消息存储在同一个表中,但通过 conversation_id 完全隔离,不会相互干扰。
initialize-schema: always 的背后:
@Bean
public DatabasePopulator databasePopulator() {
// 1. 读取 classpath 中的 initialization SQL 脚本
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("schema-h2.sql")); // 或 schema-mysql.sql
// 2. 应用启动时自动执行脚本
return populator;
}
Spring AI 根据配置的数据库类型加载对应的初始化脚本(如 schema-mysql.sql),在应用启动时自动执行。
生产环境应使用连接池,提高性能:
spring:
datasource:
url: jdbc:mysql://db-server:3306/my_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 开发环境
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always
# 生产环境 - 由 DBA 管理
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: never
-- 定期备份对话数据
BACKUP TABLE message_store TO '/backup/message_store_backup.sql';
-- 清理 7 天以前的消息
DELETE FROM message_store WHERE timestamp < UNIX_TIMESTAMP() * 1000 - 7 * 24 * 60 * 60 * 1000;
-- 定期收集表统计信息,优化查询性能
ANALYZE TABLE message_store;
-- 监控表大小增长
SELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
FROM information_schema.tables
WHERE table_schema = 'my_db' AND table_name = 'message_store';
-- 监控慢查询
SELECT * FROM mysql.general_log
WHERE command_type = 'Query' AND execution_time > 1000;
现象:启动时报错'Table already exists'
原因:通常是因为多个应用实例同时启动,都尝试创建表
解决方案:
initialize-schema: create-if-missing # 改为这个配置
现象:SQLException: Connection timeout
原因:数据库连接不可达或防火墙阻止
排查步骤:
mysql -h 127.0.0.1 -u root -pCREATE DATABASE my_db;现象:中文消息存储为乱码或"???"
原因:字符集配置不正确
解决方案:
spring:
datasource:
url: jdbc:mysql://...?useUnicode=true&characterEncoding=utf-8
jpa:
properties:
hibernate:
connection:
CharSet: utf8mb4
collation: utf8mb4_unicode_ci
现象:对话后重启应用,消息无法恢复
原因:initialize-schema 设置为 always,导致每次启动都清空表
解决方案:
# 开发环境改为:initialize-schema: create-if-missing
本文详细阐述了 Spring AI MySQL ChatMemory 的完整实现方案:
initialize-schema: always,快速迭代create-if-missing,验证数据持久化never,由 DBA 负责数据库初始化和维护从 InMemory 到 JDBC 的演进,不仅是存储介质的改变,更是架构思想的升级——从单机应用到分布式系统的支持,从临时数据到永久化存储的转变。这正是 Spring AI 框架设计的精妙之处。
希望这篇文章能帮助你充分理解 Spring AI ChatMemory 的持久化机制,在实战中灵活运用,构建更健壮、更可靠的 AI 对话应用。
相关资源

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online