ChatMemory 分布式部署实战:集群环境下的对话记忆

📅 2026/7/8 3:28:58
ChatMemory 分布式部署实战:集群环境下的对话记忆
上一篇:《你的 ChatMemory 卡顿?MySQL vs Redis 性能实测》评论区有人问:“生产环境肯定要集群部署,你的方案支持吗?”我想了想,这是个好问题。单机部署是 Demo,生产环境要集群,多个实例如何保证对话不丢失、记忆不分裂?这篇就讲 ChatMemory 的分布式部署方案,两种实战方案,代码我都写好了,直接能用!一、为什么需要分布式部署?单机部署的问题❌想象一下,你的 ChatGPT 应用部署了2 个实例:用户 A 用户 B ↓ ↓ ┌─────────┐ ┌─────────┐ │ 实例 1 │ │ 实例 2 │ └─────────┘ └─────────┘ ↓ ↓ 内存:无状态 内存:无状态问题来了:用户 A 的对话记录只存在实例 1 的内存里用户 B 访问时,被路由到实例 2用户 A 切换到实例 2,他的对话记录丢失了!两个实例各自独立,数据不同步更严重的问题:✅ 用户 A 切换实例 → 对话记录丢失 ❌✅ 用户 B 切换实例 → 对话记录丢失 ❌✅ 实例重启 → 内存数据清空 ❌对于生产环境,这3个问题都是不可接受的。二、分布式场景下的 ChatMemory 问题场景: 多实例部署,负载均衡负载均衡器 → 实例1 → MySQL/Redis → 实例2 → MySQL/Redis → 实例3 → MySQL/Redis核心问题:会话识别: 实例之间如何识别同一个用户?数据共享: MySQL/Redis 如何支持多实例并发写入?一致性: 实例1写入后,实例2能否立即读取?故障恢复: 实例3挂了,数据会不会丢失?解决方案: 需要分布式存储方案Session 共享方案三、方案1:Redis 集群(推荐) ⭐原理: Redis 作为分布式缓存,所有实例共享同一份数据架构图:用户 A 用户 B ↓ ↓ ┌─────────┐ ┌─────────┐ │ 实例1 │ │ 实例2 │ └─────────┘ └─────────┘ ↓ ↓ └───────┬───────┘ ↓ ┌─────────────┐ │ Redis Cluster │ │ (6节点集群) │ └─────────────┘Step 1: 创建 Redis Clusterdocker-compose.yml:version:3.8services:redis:image:redis:7-alpinecommand:redis-server --cluster-enabled yes --cluster-config-file /data/nodes.conf --cluster-node-timeout 5000 --appendonly yes --appendfsync everysec --requirepass ${REDIS_PASSWORD:-redis123}ports:-6379:6379volumes:-redis-data:/datanetworks:-app-networkhealthcheck:test:[CMD,redis-cli,-a,${REDIS_PASSWORD:-redis123},ping]interval:10stimeout:3sretries:5app:build:.ports:-8080:8080environment:-CHAT_MEMORY_TYPEredis-cluster-REDIS_PASSWORD${REDIS_PASSWORD:-redis123}depends_on:redis:condition:service_healthynetworks:-app-networkvolumes:redis-data:networks:app-network:driver:bridgeStep 2: 启动脚本# 1. 创建 Redis Clusterdocker-composeexecredis redis-cli-aredis123--clustercreate\redis:6379 redis:6380 redis:6381 redis:6382 redis:6383 redis:6384\--cluster-replicas1--cluster-yes# 2. 启动所有服务docker-composeup-d# 3. 验证集群状态docker-composeexecredis redis-cli-aredis123 cluster infoStep 3: 配置 Spring Bootapplication.yml:spring:data:redis:cluster:nodes:-redis1:6379-redis2:6379-redis3:6379-redis4:6379-redis5:6379-redis6:6379max-redirects:3password:${REDIS_PASSWORD:-redis123}lettuce:pool:max-active:8max-idle:8min-idle:0shutdown-timeout:100aof:enabled:trueappendonly:yesappendfsync:everysecai:chat:memory:type:redis-clusterStep 4: ChatMemory 配置ChatMemoryConfig.java:ConfigurationpublicclassChatMemoryConfig{BeanConditionalOnProperty(namespring.ai.chat.memory.type,havingValueredis-cluster,matchIfMissingfalse)PrimarypublicRedisTemplateString,ObjectredisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplateString,ObjecttemplatenewRedisTemplate();template.setConnectionFactory(connectionFactory);// JSON 序列化ObjectMappermapperJsonMapper.builder().addModule(newJavaTimeModule()).build();template.setKeySerializer(newStringRedisSerializer());template.setValueSerializer(mapper);template.setHashKeySerializer(newStringRedisSerializer());template.setHashValueSerializer(mapper);template.afterPropertiesSet();returntemplate;}BeanConditionalOnProperty(namespring.ai.chat.memory.type,havingValueredis-cluster,matchIfMissingfalse)PrimarypublicChatMemoryRepositoryredisChatMemoryRepository(Qualifier(redisTemplate)RedisTemplateString,ObjectredisTemplate){RedisChatMemoryRepositoryrepositorynewRedisChatMemoryRepository(redisTemplate);repository.expire(default,7*24*3600);returnrepository;}BeanConditionalOnProperty(namespring.ai.chat.memory.type,havingValueredis-cluster,matchIfMissingfalse)PrimarypublicChatMemoryredisChatMemory(ChatMemoryRepositoryrepository){returnMessageWindowChatMemory.builder().chatMemoryRepository(repository).maxMessages(20).build();}}Step 5: 启动测试CHAT_MEMORY_TYPEredis-clusterjava-jarhello-ai.jarcurlhttp://localhost:8080/perf/write?count1000四、方案2:MySQL Session 同步(完整版) ⭐⭐原理: 所有实例共用同一个 MySQL 数据库,通过 Redis 缓存 Conversation ID架构图:用户 A 用户 B ↓ ↓ ┌─────────┐ ┌─────────┐ │ 实例1 │ │ 实例2 │ └─────────┘ └─────────┘ ↓ ↓ └───────┬───────┘ ↓ ┌─────────────┐ │ Redis │ ← 缓存 Conversation ID │ (Session) │ └─────────────┘ ↓ ┌─────────────┐ │ MySQL │ ← 持久化对话记录 │ (DB) │ └─────────────┘Step 1: Session 管理UserSessionContext.java:publicclassUserSessionContext{privatestaticfinalThreadLocalStringSESSION_IDnewThreadLocal();privatestaticfinalThreadLocalStringUSER_IDnewThreadLocal();publicstaticvoidsetUserId(StringuserId){USER_ID.set(userId);}publicstaticStringgetUserId(){returnUSER_ID.get();}publicstaticvoidsetSessionId(StringsessionId){SESSION_ID.set(sessionId);}publicstaticStringgetSessionId(){returnSESSION_ID.get();}publicstaticvoidclear(){SESSION_ID.remove();USER_ID.remove();}}UserSessionInterceptor.java:ComponentpublicclassUserSessionInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{StringuserIdrequest.getHeader(X-User-ID);if(userIdnull||userId.isEmpty()){userIdUUID.randomUUID().toString();response.setHeader(X-User-ID,userId);}UserSessionContext.setUserId(userId);returntrue;}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{UserSessionContext.clear();}}Step 2: Conversation ID 管理ConversationService.java:ServicepublicclassConversationService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;AutowiredprivateJdbcChatMemoryRepositoryjdbcRepository;/** * 生成或获取用户的 Conversation ID * 关键:使用 user:userId:chat:last 作为 Key,所有实例共享 */publicStringgetOrCreateConversationId(StringuserId){Stringkeyuser:userId:chat:last;StringconversationId(String)redisTemplate.opsForValue().get(key);if(conversationIdnull||conversationId.isEmpty()){conversationIdconv:userId:System.currentTimeMillis();redisTemplate.opsForValue().set(key,conversationId,7,TimeUnit.DAYS);}returnconversationId;}publicvoidsaveMessage(StringuserId,Messagemessage){StringconversationIdgetOrCreateConversationId(userId);jdbcRepository.saveAll(conversationId,List.of(message));}publicListMessagegetHistory(StringuserId){StringconversationIdgetOrCreateConversationId(userId);returnjdbcRepository.findByConversationId(conversationId);}publicvoidclearConversation(StringuserId){Stringkeyuser:userId:chat:last;redisTemplate.delete(key);}}Step 3: 配置application.yml:spring:datasource:url:jdbc:mysql://localhost:3308/hello_ai?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghaiusername:${MYSQL_USER:root}password:${MYSQL_PASSWORD:root}driver-class-name:com.mysql.cj.jdbc.Driverdata:redis:host:localhostport:6379password:${REDIS_PASSWORD:}ai:chat:memory:type:mysqlStep 4: 测试CHAT_MEMORY_TYPEmysqljava-jarhello-ai.jar# 测试curl-HX-User-ID: user123http://localhost:8080/api/chat/save?message你好# 查看历史curl-HX-User-ID: user123http://localhost:8080/api/chat/history五、方案对比与选型建议对比项Redis 集群MySQL Session性能⚡⚡⚡⚡⚡ 毫秒级⚡⚡⚡ 磁盘 I/O一致性⚡⚡⚡ 最终一致⚡⚡⚡⚡⚡ 强一致持久化⚡⚡ AOF Cluster⚡⚡⚡⚡ 自动持久化扩展性⚡⚡⚡⚡ 集群扩展⚡⚡ 单机瓶颈复杂度⚡⚡ 中等⚡ 中等成本⚡ Redis 内存⚡ MySQL 磁盘选型建议:✅推荐 Redis 集群:ChatGPT 类应用,实时对话实例数量多(10)对性能要求高已有 Redis 基础设施✅推荐 MySQL Session:企业内部系统,聊天记录需长期存储实例数量少(1-5个)对一致性要求高团队熟悉关系型数据库六、总结核心结论:单机部署: 用内存版或单机 MySQL/Redis集群部署: 必须用 Redis Cluster 或 MySQL Session推荐方案: Redis 集群(简单、高性能)故障恢复: Redis Cluster MySQL 主从复制最佳实践:✅ 使用 Redis Cluster,避免单点故障✅ 配置 Redis AOF,防止数据丢失✅ 使用负载均衡,均匀分配流量✅ 监控 Redis Cluster 状态✅ 定期备份数据库写在最后我是一名 8 年 Java 后端,正在转型 AI 应用开发。Spring AI 系列会持续更新,从 hello world 到 RAG 到 Agent,一路踩坑一路写。如果你也在转型 AI,关注我,一起走。有问题评论区聊,我会逐条回复。如果这篇文章帮到了你,点个赞就是对我最大的鼓励❤️下一篇预告: ChatMemory 与 RAG 结合实战