You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/openspec/changes/storage-cache-refactor/design.md

122 lines
4.7 KiB

## Context
当前系统使用 SQLite 作为唯一存储后端,所有行情数据(`MarketData`)以 JSON 字符串形式存储在磁盘文件中。每次刷新行情时,系统需要从数据源采集数据、写入 SQLite读取时再反序列化。这种架构在高频读写场景下存在性能瓶颈。
系统包含以下核心实体:
- `MarketData`: 行情 K 线数据(高频读写,数据量大)
- `SymbolTimestamp`: 合约数据时间戳(中频读写)
- `ScheduledTask`: 定时任务配置(低频读写)
- 用户认证、交易复盘等低频业务表
约束条件:
- 单机部署,需要本地运行 Redis
- 需要保证系统可用性,即使部分存储组件故障
- 现有 SQLite 数据需要迁移或兼容
## Goals / Non-Goals
**Goals:**
- 引入 Redis 作为热数据缓存层提升行情数据读取性能TTL 30 天)
- 引入 MySQL 作为主要持久化存储,替代 SQLite 承担日常读写
- 保留 SQLite 作为最终兜底方案,保证系统可用性
- 实现三级降级策略Redis → MySQL → SQLite
- 刷新接口双写:数据同步写入 Redis 和 MySQL
- 启动时初始化多存储后端,检测可用性
**Non-Goals:**
- 不迁移用户认证、交易复盘等低频业务表(保持 SQLite
- 不改造前端页面
- 不统一 API 接口规范(留给后续 change
- 不实现分布式部署或集群方案
## Decisions
### 1. 存储分层策略
**决策**: 采用 Redis缓存+ MySQL持久化+ SQLite兜底三级架构。
**理由**:
- Redis 提供亚毫秒级读取性能,适合行情数据高频访问
- MySQL 提供可靠的持久化能力和事务支持,替代 SQLite 成为主存储
- SQLite 保留作为最终兜底,保证极端情况下系统仍可运行
- 三级降级保证系统可用性,避免单点故障
**备选方案**:
- Redis + PostgreSQL: PostgreSQL 功能更强大但部署复杂度更高MySQL 更轻量
- 纯 Redis: 无法满足持久化需求,数据丢失风险高
- 纯 MySQL: 缺少缓存层,读取性能提升有限
### 2. 数据访问层设计
**决策**: 新增 `StorageManager` 抽象层,封装三级降级逻辑。
**理由**:
- 业务代码无需关心底层存储实现
- 降级策略集中管理,便于维护和测试
- 可通过配置切换存储后端,支持渐进式迁移
**备选方案**:
- 直接在业务代码中实现降级逻辑: 代码重复,难以维护
- 使用 ORM 插件: 灵活性不足,难以实现复杂的降级策略
### 3. 双写一致性策略
**决策**: 刷新接口采用同步双写,先写 MySQL 再更新 Redis。
**理由**:
- MySQL 作为持久化主存储,必须先保证数据落盘
- Redis 作为缓存,写入失败不影响数据持久化
- 同步双写保证数据一致性,避免异步延迟导致的数据不一致
**备选方案**:
- 异步双写: 性能更好,但可能出现数据不一致
- 仅写 MySQLRedis 通过缓存未命中回填: 首次读取性能差
### 4. Redis 缓存策略
**决策**: 行情数据 TTL 30 天,缓存未命中时从 MySQL 读取并回填。
**理由**:
- 30 天覆盖大部分行情数据的活跃访问周期
- 缓存未命中回填保证读取性能
- TTL 过期后自动清理,避免内存无限增长
**备选方案**:
- 永不过期: 内存占用不可控
- 更短 TTL如 1 天): 缓存命中率低,频繁回源 MySQL
### 5. 降级检测机制
**决策**: 启动时检测 Redis/MySQL 可用性,运行时捕获连接异常触发降级。
**理由**:
- 启动时检测可提前发现问题,输出明确的错误日志
- 运行时降级保证系统可用性,避免服务中断
- 降级状态可通过日志监控,便于运维排查
**备选方案**:
- 仅启动时检测: 无法处理运行中服务故障
- 健康检查接口: 增加复杂度,当前场景不需要
### 6. 数据迁移策略
**决策**: 新增 MySQL 表结构,启动时从 SQLite 迁移历史数据到 MySQL。
**理由**:
- 保证历史数据不丢失
- 一次性迁移,后续直接读写 MySQL
- SQLite 保留作为兜底,不删除
**备选方案**:
- 手动迁移: 容易遗漏,风险高
- 双写过渡期: 复杂度高,当前场景不需要
## Risks / Trade-offs
- **[风险]** Redis 和 MySQL 同时故障 → **[缓解]** SQLite 兜底,系统仍可运行
- **[风险]** 双写一致性MySQL 写入成功但 Redis 写入失败 → **[缓解]** Redis 写入失败不影响持久化,下次读取时从 MySQL 回填
- **[风险]** Redis 内存占用过高 → **[缓解]** TTL 30 天自动清理,监控内存使用
- **[风险]** MySQL 部署复杂度增加 → **[缓解]** 使用 docker-compose 一键部署
- **[取舍]** 同步双写性能略低于异步双写 → 换取数据一致性保证
- **[取舍]** 三级降级增加代码复杂度 → 换取系统高可用性