|
|
|
|
|
## 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 作为缓存,写入失败不影响数据持久化
|
|
|
|
|
|
- 同步双写保证数据一致性,避免异步延迟导致的数据不一致
|
|
|
|
|
|
|
|
|
|
|
|
**备选方案**:
|
|
|
|
|
|
- 异步双写: 性能更好,但可能出现数据不一致
|
|
|
|
|
|
- 仅写 MySQL,Redis 通过缓存未命中回填: 首次读取性能差
|
|
|
|
|
|
|
|
|
|
|
|
### 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 一键部署
|
|
|
|
|
|
- **[取舍]** 同步双写性能略低于异步双写 → 换取数据一致性保证
|
|
|
|
|
|
- **[取舍]** 三级降级增加代码复杂度 → 换取系统高可用性
|