四、数据持久化方案
目前的状态管理最大的问题就是:应用重启,猫就饿死了(数据全丢了)。作为一只负责任的铲屎官,我们得给猫咪的状态找个靠谱的家。
4.1 方案对比
| 方案 | 优点 | 缺点 | 适用场景 | 推荐度 |
|---|---|---|---|---|
| 内存 Map(现状) | 简单、快、零配置 | 重启丢失、不支持多实例、无 TTL | 本地演示、开发调试 | ⭐ |
| Redis | 持久化、高性能、原生支持 TTL、数据结构丰富 | 需要额外部署 Redis 服务 | 生产环境首选 | ⭐⭐⭐⭐⭐ |
| 关系型数据库(MySQL/PostgreSQL) | 结构化、易查询、事务支持 | 速度稍慢、需要表设计 | 需要复杂查询、报表统计 | ⭐⭐⭐⭐ |
| 文件存储(JSON/YAML) | 简单、无需外部依赖 | 并发差、不适合高频读写 | 单机轻量应用、配置存储 | ⭐⭐ |
| H2 / SQLite(嵌入式数据库) | 零配置、持久化、SQL 支持 | 并发性能一般 | 中小型应用、快速原型 | ⭐⭐⭐ |
4.2 推荐方案:Redis + 持久化
对于这个场景,Redis 是比较 sweet 的选择:
** Redis**
- 数据结构匹配:宠物状态就是简单的 key-value(Hash),Redis 天生擅长
- TTL 自动清理:支持设置过期时间,长时间不活跃的会话自动清理,省内存
- 性能极好:读写都是微秒级,不会影响 AI 交互的响应速度
- Spring Boot 集成简单:
spring-boot-starter-data-redis一行依赖搞定- 支持集群:应用多实例部署时,Redis 是共享状态的最佳选择
- 持久化选项:RDB 快照 + AOF 日志,数据不会丢
4.3 Redis 数据结构设计
Key 设计规范:
prefix : 业务标识 separator : ":" namespace : 状态类型 id : 会话 ID 完整格式 : pet:state:{conversationId} 示例 : pet:state:user-123-abcHash 字段设计:
| 字段 | 类型 | 说明 |
|---|---|---|
hunger | Integer | 饥饿度 0-100 |
happiness | Integer | 开心度 0-100 |
lastInteractionTime | ISO-8601 String | 上次互动时间 |
mood | String | 当前心情枚举值 |
version | Integer | 乐观锁版本号(防止并发覆盖) |
TTL 策略:
- 默认 TTL:7 天(
604800秒) - 每次互动后刷新 TTL
- 长时间不活跃的宠物自动"放生"(清理数据)
4.4 存储层代码实现
// PetStateRepository.java - 存储接口publicinterfacePetStateRepository{Optional<PetState>findById(StringconversationId);voidsave(PetStatestate);voiddeleteById(StringconversationId);}// RedisPetStateRepository.java - Redis 实现@Repository@PrimarypublicclassRedisPetStateRepositoryimplementsPetStateRepository{privatefinalStringRedisTemplateredisTemplate;privatefinalObjectMapperobjectMapper;privatestaticfinalStringKEY_PREFIX="pet:state:";privatestaticfinallongTTL_SECONDS=7*24*60*60;// 7天publicRedisPetStateRepository(StringRedisTemplateredisTemplate,ObjectMapperobjectMapper){this.redisTemplate=redisTemplate;this.objectMapper=objectMapper;}@OverridepublicOptional<PetState>findById(StringconversationId){Stringkey=KEY_PREFIX+conversationId;// 使用 Hash 操作获取所有字段Map<Object,Object>entries=redisTemplate.opsForHash().entries(key);if(entries.isEmpty()){returnOptional.empty();}returnOptional.of(mapToPetState(conversationId,entries));}@Overridepublicvoidsave(PetStatestate){Stringkey=KEY_PREFIX+state.getConversationId();// 使用 Hash 存储,字段清晰,更新灵活Map<String,String>map=newHashMap<>();map.put("hunger",String.valueOf(state.getHunger()));map.put("happiness",String.valueOf(state.getHappiness()));map.put("lastInteractionTime",state.getLastInteractionTime().toString());map.put("mood",state.getMood().name());// 使用 putAll 原子性写入redisTemplate.opsForHash().putAll(key,map);// 刷新 TTLredisTemplate.expire(key,TTL_SECONDS,TimeUnit.SECONDS);}@OverridepublicvoiddeleteById(StringconversationId){redisTemplate.delete(KEY_PREFIX+conversationId);}/** * 将 Redis Hash 映射为 PetState 对象 */privatePetStatemapToPetState(StringconversationId,Map<Object,Object>entries){PetStatestate=newPetState(conversationId);if(entries.containsKey("hunger")){state.setHunger(Integer.parseInt(entries.get("hunger").toString()));}if(entries.containsKey("happiness")){state.setHappiness(Integer.parseInt(entries.get("happiness").toString()));}if(entries.containsKey("lastInteractionTime")){state.setLastInteractionTime(LocalDateTime.parse(entries.get("lastInteractionTime").toString()));}if(entries.containsKey("mood")){state.setMood(PetMood.valueOf(entries.get("mood").toString()));}returnstate;}}4.5 降级方案:内存实现(用于开发测试)
// InMemoryPetStateRepository.java - 内存实现(开发/测试用)@Repository@Profile("dev")// 只在 dev 环境生效publicclassInMemoryPetStateRepositoryimplementsPetStateRepository{privatefinalMap<String,PetState>store=newConcurrentHashMap<>();@OverridepublicOptional<PetState>findById(StringconversationId){returnOptional.ofNullable(store.get(conversationId));}@Overridepublicvoidsave(PetStatestate){store.put(state.getConversationId(),state);}@OverridepublicvoiddeleteById(StringconversationId){store.remove(conversationId);}}五、定时任务设计
猫咪不是机器,它需要**"活着"的感觉**。即使主人不在线,它也应该有自己的生活规律——会饿、会无聊、会想主人。