--- comet_change: storage-cache-refactor role: technical-design canonical_spec: openspec --- # Storage Cache Refactor - Technical Design ## 1. 架构概览 ### 1.1 目标架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 应用层 │ │ app/api/data.py ←→ app/services/cache.py │ │ ↓ │ │ StorageManager │ │ (storage_manager.py) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────┼─────────────────────┐ ↓ ↓ ↓ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Redis │ │ MySQL │ │ SQLite │ │ (缓存) │ │(持久化) │ │ (兜底) │ └─────────┘ └─────────┘ └─────────┘ ``` ### 1.2 核心组件 | 组件 | 文件 | 职责 | |------|------|------| | StorageManager | `app/storage_manager.py` | 封装三级存储逻辑,提供统一接口 | | RedisClient | `app/redis_client.py` | Redis 连接池和客户端封装 | | MySQLDatabase | `app/mysql_database.py` | MySQL 引擎和 SessionLocal | | cache.py | `app/services/cache.py` | 保持现有函数签名,内部调用 StorageManager | ## 2. Redis 数据结构 ### 2.1 行情数据缓存 ``` Key: market_data:{symbol}:{period} Value: JSON { "current_price": 123.45, "timestamp": "2026-07-04T10:00:00", "candles": [ {"datetime": "...", "open": ..., "high": ..., "low": ..., "close": ...} ] } TTL: 30 天 (2592000 秒) ``` ### 2.2 合约时间戳缓存 ``` Key: symbol_timestamps:{symbol} Value: JSON { "last_refresh_at": "2026-07-04T10:00:00", "refresh_count": 42 } TTL: 30 天 (2592000 秒) ``` ### 2.3 设计理由 - 结构化键值存储,便于按品种和周期精确查询 - JSON 格式与当前 SQLite 存储格式兼容,迁移成本低 - TTL 自动清理,避免内存无限增长 ## 3. 数据流设计 ### 3.1 读取流程 ``` 请求行情数据 ↓ 检查 Redis 缓存 ├─ 命中 → 返回数据 └─ 未命中 → 检查 MySQL 可用性 ├─ 可用 → 读取 MySQL → 回填 Redis (TTL 30天) → 返回 └─ 不可用 → 读取 SQLite → 返回 ``` ### 3.2 写入流程(刷新接口) ``` 刷新行情数据 ↓ 删除 Redis 缓存 (market_data:{symbol}:{period}) ↓ 写入 MySQL(事务) ├─ 成功 → 更新 Redis 缓存 → 返回成功 └─ 失败 → 返回错误(不更新 Redis) ``` ### 3.3 降级流程 ``` StorageManager 检查存储后端可用性 ↓ Redis 可用? ├─ 是 → 使用 Redis 缓存 └─ 否 → MySQL 可用? ├─ 是 → 使用 MySQL 持久化 └─ 否 → 使用 SQLite 兜底 ``` ## 4. 降级检测机制 ### 4.1 惰性恢复策略 ```python class StorageManager: def __init__(self): self.redis_available = False self.mysql_available = False self.last_redis_check = 0 self.last_mysql_check = 0 self.check_interval = 30 # 秒 def check_redis(self): """检查 Redis 可用性,30秒内不重复检测""" now = time.time() if now - self.last_redis_check < self.check_interval: return self.redis_available try: self.redis_client.ping() self.redis_available = True logger.info("Redis 连接恢复") except Exception as e: self.redis_available = False logger.warning(f"Redis 不可用: {e}") self.last_redis_check = now return self.redis_available def check_mysql(self): """检查 MySQL 可用性,30秒内不重复检测""" now = time.time() if now - self.last_mysql_check < self.check_interval: return self.mysql_available try: with self.mysql_engine.connect() as conn: conn.execute(text("SELECT 1")) self.mysql_available = True logger.info("MySQL 连接恢复") except Exception as e: self.mysql_available = False logger.warning(f"MySQL 不可用: {e}") self.last_mysql_check = now return self.mysql_available ``` ### 4.2 启动时初始化 ```python # app/main.py lifespan storage_manager = StorageManager() storage_manager.initialize() # 检测可用性 redis_ok = storage_manager.check_redis() mysql_ok = storage_manager.check_mysql() if redis_ok and mysql_ok: logger.info("存储模式: Redis + MySQL") elif mysql_ok: logger.warning("存储模式: MySQL (Redis 不可用)") else: logger.error("存储模式: SQLite (Redis 和 MySQL 均不可用)") ``` ## 5. 集成方式 ### 5.1 cache.py 内部封装 ```python # app/services/cache.py def get_cached_data(db, symbol, data_type, periods, end_time=None, max_candles=100): """从缓存中获取完整的多周期数据""" storage = get_storage_manager() # 优先从 Redis/MySQL 读取 if storage.is_available(): try: result = storage.get_market_data(symbol, data_type, periods) if result: return result except Exception as e: logger.warning(f"StorageManager 读取失败,降级到 SQLite: {e}") # 降级到 SQLite return _get_from_sqlite(db, symbol, data_type, periods, end_time, max_candles) def save_market_data(db, symbol, data): """保存采集结果到缓存""" storage = get_storage_manager() # 优先写入 Redis/MySQL if storage.is_available(): try: storage.save_market_data(symbol, data) return except Exception as e: logger.warning(f"StorageManager 写入失败,降级到 SQLite: {e}") # 降级到 SQLite _save_to_sqlite(db, symbol, data) ``` ### 5.2 API 层零改动 - `app/api/data.py` 保持不变 - 所有接口仍使用 `db: Session = Depends(get_db)` - cache.py 内部自动选择存储后端 ## 6. 数据迁移 ### 6.1 迁移策略 ```python # app/migration.py def migrate_sqlite_to_mysql(): """从 SQLite 迁移历史数据到 MySQL""" sqlite_engine = create_engine(f"sqlite:///{DB_PATH}") mysql_engine = create_mysql_engine() # 检查 MySQL 表是否为空 with mysql_engine.connect() as conn: result = conn.execute(text("SELECT COUNT(*) FROM market_data")) count = result.scalar() if count > 0: logger.info("MySQL 已有数据,跳过迁移") return # 从 SQLite 读取数据 with sqlite_engine.connect() as conn: result = conn.execute(text("SELECT * FROM market_data")) rows = result.fetchall() # 写入 MySQL with mysql_engine.begin() as conn: for row in rows: conn.execute( text("INSERT INTO market_data ..."), {...} ) logger.info(f"数据迁移完成,共迁移 {len(rows)} 条记录") ``` ### 6.2 迁移触发时机 - 应用启动时自动检测 - MySQL 表为空时触发迁移 - 迁移完成后输出日志 ## 7. 配置项 ### 7.1 新增配置 ```python # app/config.py # Redis 配置 REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) REDIS_DB = int(os.getenv("REDIS_DB", "0")) REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") REDIS_TTL_SECONDS = int(os.getenv("REDIS_TTL_SECONDS", "2592000")) # 30 天 # MySQL 配置 MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) MYSQL_USER = os.getenv("MYSQL_USER", "root") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "") MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "buffer_platform") ``` ### 7.2 docker-compose.yml 新增服务 ```yaml services: redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis-data:/data mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql ``` ## 8. 测试策略 ### 8.1 单元测试 - StorageManager 各方法独立测试 - Redis 缓存命中/未命中场景 - MySQL 读写场景 - 降级逻辑场景 ### 8.2 集成测试 - Redis + MySQL 正常模式 - Redis 不可用降级到 MySQL - Redis + MySQL 均不可用降级到 SQLite - 刷新接口双写一致性 ### 8.3 故障注入 - 模拟 Redis 服务停止 - 模拟 MySQL 服务停止 - 验证降级和恢复逻辑 ### 8.4 性能测试 - 对比改造前后读取延迟 - 验证 Redis 缓存命中率 - 监控 MySQL 查询性能 ## 9. 风险与缓解 | 风险 | 影响 | 缓解措施 | |------|------|---------| | Redis 内存占用过高 | 系统内存不足 | TTL 30 天自动清理,监控内存使用 | | 双写一致性 | MySQL 成功但 Redis 失败 | Redis 失败仅记录日志,不影响持久化 | | 降级检测延迟 | 恢复不及时 | 30 秒惰性恢复阈值,平衡性能和实时性 | | 数据迁移失败 | 历史数据丢失 | 保留 SQLite 兜底,可手动回滚 | | MySQL 部署复杂度 | 运维成本增加 | docker-compose 一键部署 | ## 10. 实施计划 ### 10.1 阶段划分 1. **依赖与配置**: 添加 redis、pymysql 依赖,新增配置项 2. **数据库模型**: 创建 Redis/MySQL 连接模块 3. **StorageManager**: 实现三级存储逻辑 4. **集成改造**: 改造 cache.py,集成 StorageManager 5. **数据迁移**: 实现 SQLite → MySQL 迁移 6. **测试验证**: 单元测试、集成测试、故障注入 ### 10.2 验收标准 - Redis 缓存命中时,读取延迟 < 10ms - Redis 未命中时,从 MySQL 读取并回填 - Redis 不可用时,自动降级到 MySQL - Redis + MySQL 均不可用时,降级到 SQLite - 刷新接口双写成功,数据一致性保证 - 数据迁移完整,历史数据不丢失