docs: add storage-cache-refactor openspec artifacts, design doc and implementation plan

refactor3.0
Lxy 2 weeks ago
parent 6eecbcbbf3
commit a85f61a927

@ -0,0 +1,6 @@
# context_compression: off | beta
context_compression: off
# review_mode: off | standard | thorough
review_mode: off
# auto_transition: true | false
auto_transition: true

@ -0,0 +1,447 @@
---
change: storage-cache-refactor
design-doc: docs/superpowers/specs/2026-07-04-storage-cache-refactor-design.md
base-ref: 8b5e43f4915cef786208a8132460f87b8b191765
---
# Storage Cache Refactor - 实施计划
> 基于 [Design Doc](docs/superpowers/specs/2026-07-04-storage-cache-refactor-design.md) 拆分的可执行任务。
> 目标:引入 Redis缓存+ MySQL持久化+ SQLite兜底三级存储架构改造现有 cache.py 内部封装 StorageManagerAPI 层零改动。
---
## 任务总览
| # | 任务 | 预估 | 依赖 |
|---|------|------|------|
| T1 | 添加 redis/pymysql 依赖 | 5min | - |
| T2 | config.py 新增 Redis/MySQL 配置项 | 10min | - |
| T3 | docker-compose.yml 新增 Redis/MySQL 服务 | 10min | T2 |
| T4 | 创建 app/mysql_database.py | 20min | T2 |
| T5 | 创建 app/redis_client.py | 20min | T2 |
| T6 | lifespan 中初始化 Redis/MySQL 连接 | 15min | T4, T5 |
| T7 | MySQL 表结构初始化(复用 ORM 模型) | 15min | T4, T6 |
| T8 | 创建 app/storage_manager.py 骨架 + 降级检测 | 30min | T4, T5, T6 |
| T9 | StorageManager 读取逻辑Redis → MySQL → SQLite | 30min | T8 |
| T10 | StorageManager 写入逻辑 + 双写一致性 | 30min | T8 |
| T11 | 创建数据迁移脚本 app/migration.py | 25min | T4, T7 |
| T12 | 启动时自动触发迁移 | 10min | T11 |
| T13 | 改造 cache.py 内部封装 StorageManager | 30min | T9, T10 |
| T14 | 验证Redis 缓存命中/未命中场景 | 15min | T13 |
| T15 | 验证:降级到 MySQL / SQLite 场景 | 15min | T13 |
| T16 | 验证:双写一致性 + 数据迁移完整性 | 15min | T13 |
---
## 详细任务
### T1: 添加 redis/pymysql 依赖
**文件**: `requirements.txt`
**操作**:
- 在 `requirements.txt` 末尾追加:
```
redis>=5.0.0
pymysql>=1.1.0
cryptography>=42.0.0
```
- `cryptography` 是 PyMySQL 启用加密连接所需的可选依赖
**验证**: `pip install -r requirements.txt` 成功安装无报错
---
### T2: config.py 新增 Redis/MySQL 配置项
**文件**: `app/config.py`
**操作**:
- 在现有配置末尾新增以下配置块(参考 Design Doc §7.1
```python
# 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", "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")
```
**验证**: `from app.config import REDIS_HOST, MYSQL_HOST` 无报错,默认值正确
---
### T3: docker-compose.yml 新增 Redis/MySQL 服务
**文件**: `docker-compose.yml`
**操作**:
- 在 `services` 下新增 `redis``mysql` 服务(参考 Design Doc §7.2
```yaml
redis:
image: redis:7-alpine
container_name: futures-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- futures-network
mysql:
image: mysql:8.0
container_name: futures-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-buffer123}
MYSQL_DATABASE: ${MYSQL_DATABASE:-buffer_platform}
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
networks:
- futures-network
```
- 在顶层新增 `volumes` 声明:
```yaml
volumes:
redis-data:
mysql-data:
```
- 在 `buffer-platform``environment` 中新增 Redis/MySQL 环境变量传递
**验证**: `docker-compose config` 解析无语法错误
---
### T4: 创建 app/mysql_database.py
**文件**: `app/mysql_database.py`(新建)
**操作**:
- 创建 MySQL 连接引擎和 SessionLocal参考现有 `app/database.py` 的结构:
```python
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.config import MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE
mysql_engine = None
MySQLSessionLocal = None
def init_mysql():
"""初始化 MySQL 连接引擎,失败时返回 None"""
global mysql_engine, MySQLSessionLocal
try:
url = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
mysql_engine = create_engine(url, pool_pre_ping=True, pool_recycle=3600)
with mysql_engine.connect() as conn:
conn.execute(text("SELECT 1"))
MySQLSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=mysql_engine)
return mysql_engine
except Exception as e:
logger.warning(f"MySQL 初始化失败: {e}")
mysql_engine = None
MySQLSessionLocal = None
return None
def get_mysql_session():
"""获取 MySQL 会话"""
if MySQLSessionLocal is None:
raise RuntimeError("MySQL 未初始化")
return MySQLSessionLocal()
```
**验证**: 手动调用 `init_mysql()`,在无 MySQL 环境下返回 `None` 不抛异常
---
### T5: 创建 app/redis_client.py
**文件**: `app/redis_client.py`(新建)
**操作**:
- 封装 Redis 连接池和客户端:
```python
import redis
from app.config import REDIS_HOST, REDIS_PORT, REDIS_DB, REDIS_PASSWORD
redis_client = None
def init_redis():
"""初始化 Redis 客户端,失败时返回 None"""
global redis_client
try:
redis_client = redis.Redis(
host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB,
password=REDIS_PASSWORD or None,
decode_responses=True,
socket_connect_timeout=5,
)
redis_client.ping()
return redis_client
except Exception as e:
logger.warning(f"Redis 初始化失败: {e}")
redis_client = None
return None
def get_redis():
"""获取 Redis 客户端实例"""
return redis_client
```
**验证**: 手动调用 `init_redis()`,在无 Redis 环境下返回 `None` 不抛异常
---
### T6: lifespan 中初始化 Redis/MySQL 连接
**文件**: `app/main.py`
**操作**:
- 在 `lifespan` 函数中,`Base.metadata.create_all` 之后新增:
```python
# 初始化 Redis 和 MySQL
from app.redis_client import init_redis
from app.mysql_database import init_mysql
redis_ok = init_redis() is not None
mysql_ok = init_mysql() is not None
if redis_ok and mysql_ok:
logger.info("存储模式: Redis + MySQL")
elif mysql_ok:
logger.warning("存储模式: MySQL (Redis 不可用)")
else:
logger.error("存储模式: SQLite (Redis 和 MySQL 均不可用)")
```
**验证**: 应用启动日志中输出存储模式信息,无 Redis/MySQL 时降级提示正常
---
### T7: MySQL 表结构初始化(复用 ORM 模型)
**文件**: `app/main.py`(修改 lifespan
**操作**:
- 在 T6 初始化代码之后,如果 MySQL 可用,使用现有 ORM 模型创建表:
```python
if mysql_ok:
from app.mysql_database import mysql_engine
from app.models import Base as MarketBase
# 在 MySQL 中创建行情相关表
MarketBase.metadata.create_all(bind=mysql_engine)
logger.info("MySQL 表结构初始化完成")
```
- 注意:只需创建 `market_data``symbol_timestamps` 两张表,`scheduled_tasks` 等低频表仍留在 SQLite
**验证**: MySQL 中可查到 `market_data``symbol_timestamps` 表结构
---
### T8: 创建 app/storage_manager.py 骨架 + 降级检测
**文件**: `app/storage_manager.py`(新建)
**操作**:
- 实现 `StorageManager` 类骨架,包含:
- `__init__`: 持有 redis_client / mysql_engine 引用,初始化降级状态标志
- `check_redis()` / `check_mysql()`: 惰性恢复检测30 秒间隔,参考 Design Doc §4.1
- `is_available()`: 判断 Redis 或 MySQL 至少一个可用
- `initialize()`: 启动时调用,检测各后端可用性并输出日志
- 全局单例 `get_storage_manager()` 工厂函数
- 本任务只实现骨架和降级检测,不包含具体读写逻辑
**验证**: 实例化 `StorageManager` 后,`check_redis()` / `check_mysql()` 在无服务环境下返回 `False`
---
### T9: StorageManager 读取逻辑Redis → MySQL → SQLite
**文件**: `app/storage_manager.py`(续写)
**操作**:
- 实现 `get_market_data(symbol, data_type, periods)` 方法:
1. 检查 Redis 缓存key 格式:`market_data:{symbol}:{period}`,参考 Design Doc §2.1
2. Redis 命中 → 反序列化 JSON 返回
3. Redis 未命中 → 检查 MySQL 可用性
4. MySQL 可用 → 从 MySQL 读取 → 回填 RedisTTL 30 天)→ 返回
5. MySQL 不可用 → 返回 `None`(由调用方降级到 SQLite
- 实现 `get_symbol_timestamp(symbol, data_type)` 方法:
- 类似逻辑key 格式:`symbol_timestamps:{symbol}`
**验证**: 单元测试或手动测试:
- 无 Redis/MySQL 时返回 `None`
- 有 MySQL 时从 MySQL 读取并回填 Redis
---
### T10: StorageManager 写入逻辑 + 双写一致性
**文件**: `app/storage_manager.py`(续写)
**操作**:
- 实现 `save_market_data(symbol, data)` 方法(参考 Design Doc §3.2
1. 删除 Redis 缓存(`market_data:{symbol}:{period}`
2. 写入 MySQL事务
3. MySQL 成功 → 更新 Redis 缓存
4. MySQL 失败 → 记录日志,返回 False不更新 Redis
5. Redis 更新失败 → 仅记录日志,不影响返回
- 实现 `save_symbol_timestamp(symbol, data_type, refresh_time)` 方法:
- 类似双写逻辑
- 实现 `delete_cache(symbol, periods)` 方法:
- 批量删除 Redis 缓存键
**验证**:
- MySQL 写入成功 → Redis 缓存同步更新
- MySQL 写入失败 → Redis 不更新,返回 False
- Redis 更新失败 → 不影响 MySQL 写入结果
---
### T11: 创建数据迁移脚本 app/migration.py
**文件**: `app/migration.py`(新建)
**操作**:
- 实现 `migrate_sqlite_to_mysql()` 函数(参考 Design Doc §6.1
1. 创建 SQLite 引擎(复用 `DB_PATH`
2. 检查 MySQL `market_data` 表是否为空
3. 为空时从 SQLite 读取全部 `market_data` 记录
4. 批量写入 MySQL
5. 同样迁移 `symbol_timestamps`
6. 输出迁移记录数日志
- 幂等设计MySQL 已有数据时跳过
**验证**:
- MySQL 表为空时,迁移成功,数据条数一致
- MySQL 表非空时,跳过迁移,日志提示
---
### T12: 启动时自动触发迁移
**文件**: `app/main.py`(修改 lifespan
**操作**:
- 在 T7 表结构初始化之后新增:
```python
if mysql_ok:
from app.migration import migrate_sqlite_to_mysql
migrate_sqlite_to_mysql()
```
**验证**: 首次启动时日志输出迁移信息,再次启动时跳过迁移
---
### T13: 改造 cache.py 内部封装 StorageManager
**文件**: `app/services/cache.py`(修改)
**操作**:
- 改造 `save_market_data()` 函数(参考 Design Doc §5.1
- 优先调用 `storage.save_market_data(symbol, data)` 双写
- 失败时降级到现有 SQLite 写入逻辑 `_save_to_sqlite()`
- 改造 `get_cached_data()` 函数:
- 优先调用 `storage.get_market_data(symbol, data_type, periods)` 读取
- 返回 `None` 时降级到现有 SQLite 读取逻辑 `_get_from_sqlite()`
- 改造 `update_symbol_timestamp()``get_symbol_timestamp()`
- 类似封装,优先走 StorageManager
- 保持所有函数的外部签名不变API 层零改动
- 现有 SQLite 逻辑提取为内部 `_save_to_sqlite()` / `_get_from_sqlite()` 私有函数
**验证**:
- 现有 API 接口 `/api/v1/data/latest/{symbol}` 返回数据格式不变
- 现有 API 接口 `/api/v1/data/batch-fetch` 行为不变
- 无 Redis/MySQL 时完全降级到 SQLite行为与改造前一致
---
### T14: 验证 - Redis 缓存命中/未命中场景
**操作**:
- 启动 Redis + MySQL 服务
- 调用刷新接口写入数据
- 调用读取接口,验证首次从 MySQL 读取并回填 Redis
- 再次调用读取接口,验证从 Redis 缓存命中(检查日志)
- 等待 TTL 过期或手动删除 Redis key验证回源 MySQL
**验证标准**:
- 日志中可见 "Redis 缓存命中" / "Redis 未命中,从 MySQL 读取" 信息
- 返回数据格式正确
---
### T15: 验证 - 降级到 MySQL / SQLite 场景
**操作**:
- 停止 Redis 服务,调用读取接口
- 验证自动降级到 MySQL日志输出 "Redis 不可用"
- 停止 Redis + MySQL 服务,调用读取接口
- 验证降级到 SQLite日志输出 "存储模式: SQLite"
- 恢复 Redis 服务,等待 30 秒后验证自动恢复
**验证标准**:
- 各降级场景下接口正常返回,无 500 错误
- 日志清晰标识当前存储模式
---
### T16: 验证 - 双写一致性 + 数据迁移完整性
**操作**:
- 双写一致性:
1. 调用刷新接口写入数据
2. 分别查询 MySQL 和 Redis验证数据一致
3. 模拟 Redis 写入失败(停止 Redis验证 MySQL 数据不受影响
- 数据迁移完整性:
1. 清空 MySQL 表
2. 重启应用触发迁移
3. 对比 SQLite 和 MySQL 数据条数一致
**验证标准**:
- MySQL 和 Redis 中数据内容一致
- Redis 故障不影响 MySQL 持久化
- 迁移前后数据条数一致
---
## 依赖关系图
```
T1 (依赖) ──┐
T2 (配置) ──┼── T3 (docker-compose)
├── T4 (mysql_database.py)
└── T5 (redis_client.py)
T4 + T5 ─────── T6 (lifespan 初始化)
T7 (MySQL 建表)
T8 (StorageManager 骨架)
T9 T10 (读/写逻辑)
T13 (cache.py 改造)
│ ╲
T14 T15 T16 (验证)
T4 + T7 ──── T11 (迁移脚本) ──── T12 (启动迁移)
```
## 关键约束
1. **API 层零改动**: `app/api/data.py` 不修改,所有变更封装在 `cache.py``storage_manager.py` 内部
2. **SQLite 保留**: 不删除 SQLite 相关代码,作为最终兜底
3. **幂等迁移**: 数据迁移脚本可重复执行MySQL 有数据时跳过
4. **渐进式降级**: 无 Redis/MySQL 时系统行为与改造前完全一致

@ -0,0 +1,367 @@
---
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", "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
- 刷新接口双写成功,数据一致性保证
- 数据迁移完整,历史数据不丢失

@ -0,0 +1,22 @@
workflow: full
phase: build
context_compression: off
build_mode: subagent-driven-development
build_pause: null
subagent_dispatch: confirmed
tdd_mode: tdd
review_mode: thorough
isolation: worktree
verify_mode: null
auto_transition: true
base_ref: 8b5e43f4915cef786208a8132460f87b8b191765
design_doc: docs/superpowers/specs/2026-07-04-storage-cache-refactor-design.md
plan: docs/superpowers/plans/2026-07-04-storage-cache-refactor.md
verify_result: pending
verification_report: null
branch_status: pending
created_at: 2026-07-04
verified_at: null
archived: false
handoff_context: openspec/changes/storage-cache-refactor/.comet/handoff/design-context.json
handoff_hash: 1064fdf5186e511848878541bdad663ac6b521ee5721807d01eaf3481bb0cd5a

@ -0,0 +1,46 @@
# Brainstorm Summary
- Change: storage-cache-refactor
- Date: 2026-07-04
## 确认的技术方案
1. **Redis 数据结构**: 方案 C — 结构化键值存储
- `market_data:{symbol}:{period}` → JSON含 current_price, timestamp, candles
- `symbol_timestamps:{symbol}` → JSON含 last_refresh_at, refresh_count
- TTL: 30 天
2. **集成方式**: 方案 B — cache.py 内部封装
- cache.py 函数签名不变(仍接收 `db: Session`
- 函数内部判断:优先走 Redis/MySQL降级时走原有 SQLite 逻辑
- API 层零改动,渐进式迁移
3. **降级检测**: 方案 C — 惰性恢复
- 启动时检测 Redis/MySQL 可用性
- 运行时捕获异常标记不可用
- 下次请求时距上次检测超 30 秒则尝试重连
- 重连成功恢复,失败继续降级
4. **MySQL 驱动**: pymysql纯 PythonWindows 零障碍部署)
5. **双写策略**: 同步双写,先写 MySQL 再更新 Redis
- 写入流程:删 Redis 缓存 → 写 MySQL → 更新 Redis
- MySQL 失败回滚Redis 失败仅记录日志
## 关键取舍与风险
- Redis 内存占用 → TTL 30 天自动清理
- 双写一致性 → MySQL 优先Redis 失败不影响持久化
- 降级检测延迟 → 30 秒惰性恢复阈值
- 数据迁移 → 启动时自动从 SQLite 迁移到 MySQL保留 SQLite 兜底
## 测试策略
- 单元测试StorageManager 各方法
- 集成测试:三级降级场景
- 故障注入:模拟 Redis/MySQL 不可用
- 性能对比:改造前后读取延迟
## Spec Patch
无(现有 delta spec 已覆盖所有验收场景)

@ -0,0 +1,17 @@
{
"change": "storage-cache-refactor",
"phase": "design",
"mode": "compact",
"canonical_spec": "openspec",
"generated_by": "comet-handoff.sh",
"context_hash": "1064fdf5186e511848878541bdad663ac6b521ee5721807d01eaf3481bb0cd5a",
"files": [
{ "path": "openspec/changes/storage-cache-refactor/proposal.md", "sha256": "759de20d4e94b3228ee94d8607740de2949c24935933884584b85d3d855569ad" },
{ "path": "openspec/changes/storage-cache-refactor/design.md", "sha256": "293fba52d3bef2adc55fa3d8667879fc7806918922f2f273d05efbc870c862ac" },
{ "path": "openspec/changes/storage-cache-refactor/tasks.md", "sha256": "295733c66c5b2599cac51356e7344d17c579e674e667ec24c6b77d62ee519fcd" },
{ "path": "openspec/changes/storage-cache-refactor/specs/dual-write-consistency/spec.md", "sha256": "6fd0c00433c73329adc93f97ea5a8b744d9a74148d7db40f30c843e92480b91e" },
{ "path": "openspec/changes/storage-cache-refactor/specs/mysql-persistence/spec.md", "sha256": "f23680f51345a871dddbf3d18eacf6a53de1a20945d02516ffc55a2eb500af75" },
{ "path": "openspec/changes/storage-cache-refactor/specs/redis-cache-layer/spec.md", "sha256": "a0b5c7023c88339957ce5576ad326474fd0ab913756c223573766cbc9e23f761" },
{ "path": "openspec/changes/storage-cache-refactor/specs/storage-fallback/spec.md", "sha256": "feaf531250861c109bc48d06d684ff2520cb48269397211bdcefe7cda9013714" }
]
}

@ -0,0 +1,387 @@
# Comet Design Handoff
- Change: storage-cache-refactor
- Phase: design
- Mode: compact
- Context hash: 1064fdf5186e511848878541bdad663ac6b521ee5721807d01eaf3481bb0cd5a
Generated-by: comet-handoff.sh
OpenSpec remains the canonical capability spec. This handoff is a deterministic, source-traceable context pack, not an agent-authored summary.
## openspec/changes/storage-cache-refactor/proposal.md
- Source: openspec/changes/storage-cache-refactor/proposal.md
- Lines: 1-33
- SHA256: 759de20d4e94b3228ee94d8607740de2949c24935933884584b85d3d855569ad
```md
## Why
当前系统使用 SQLite 作为唯一存储后端,无法满足行情数据高频读写的性能需求。每次刷新行情数据都需要直接读写磁盘文件,导致响应延迟高、并发能力受限。需要引入更高效的存储方案以提升系统吞吐量和用户体验。
## What Changes
- 引入 Redis 作为热数据缓存层,行情数据优先从 Redis 读取TTL 30 天)
- 引入 MySQL 作为持久化存储层,替代 SQLite 承担主要持久化职责
- 保留 SQLite 作为最终兜底方案,当 Redis 和 MySQL 均不可用时启用
- 新增数据访问层:实现 Redis → MySQL → SQLite 的三级降级读取策略
- 刷新接口改造:数据同步写入 Redis 和 MySQL保证双写一致性
- 启动流程改造:初始化 Redis/MySQL/SQLite 连接,检测可用性
## Capabilities
### New Capabilities
- `redis-cache-layer`: Redis 缓存读写能力包括热数据缓存、TTL 管理、缓存失效策略
- `mysql-persistence`: MySQL 持久化存储能力,替代 SQLite 承担主要持久化职责
- `storage-fallback`: 三级降级策略Redis → MySQL → SQLite保证系统可用性
- `dual-write-consistency`: 刷新接口双写能力,保证 Redis 和 MySQL 数据一致性
### Modified Capabilities
(无既有 spec 需要修改)
## Impact
- **代码**: `app/database.py`、`app/models.py`、`app/api/data.py`、`app/services/collector.py`、`app/main.py`
- **依赖**: 新增 `redis`、`pymysql`(或 `aiomysql`Python 包
- **配置**: `app/config.py` 新增 Redis/MySQL 连接配置
- **部署**: `docker-compose.yml` 新增 Redis 和 MySQL 服务
- **数据库**: 新增 MySQL 数据库初始化脚本,保留 SQLite 作为兜底
```
## openspec/changes/storage-cache-refactor/design.md
- Source: openspec/changes/storage-cache-refactor/design.md
- Lines: 1-121
- SHA256: 293fba52d3bef2adc55fa3d8667879fc7806918922f2f273d05efbc870c862ac
[TRUNCATED]
```md
## 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 天覆盖大部分行情数据的活跃访问周期
```
Full source: openspec/changes/storage-cache-refactor/design.md
## openspec/changes/storage-cache-refactor/tasks.md
- Source: openspec/changes/storage-cache-refactor/tasks.md
- Lines: 1-46
- SHA256: 295733c66c5b2599cac51356e7344d17c579e674e667ec24c6b77d62ee519fcd
```md
## 1. 依赖与配置
- [ ] 1.1 添加 `redis`、`pymysql` 依赖到 `requirements.txt`
- [ ] 1.2 在 `app/config.py` 中新增 Redis 和 MySQL 连接配置项host、port、user、password、database
- [ ] 1.3 在 `docker-compose.yml` 中新增 Redis 和 MySQL 服务定义
## 2. 数据库模型与初始化
- [ ] 2.1 创建 `app/mysql_database.py`,实现 MySQL 连接引擎和 SessionLocal
- [ ] 2.2 创建 `app/redis_client.py`,实现 Redis 连接池和客户端封装
- [ ] 2.3 在 `app/main.py``lifespan` 中初始化 Redis 和 MySQL 连接,检测可用性
- [ ] 2.4 创建 MySQL 表结构初始化脚本(复用现有 ORM 模型)
## 3. 数据迁移
- [ ] 3.1 创建数据迁移脚本,从 SQLite 读取历史数据并写入 MySQL
- [ ] 3.2 在应用启动时检测 MySQL 表是否为空,自动触发迁移
## 4. 存储管理层
- [ ] 4.1 创建 `app/storage_manager.py`,实现 `StorageManager` 抽象层
- [ ] 4.2 实现 Redis 缓存读取逻辑(命中返回,未命中回源 MySQL 并回填)
- [ ] 4.3 实现 MySQL 持久化读写逻辑
- [ ] 4.4 实现三级降级策略Redis → MySQL → SQLite
- [ ] 4.5 实现降级状态检测和恢复机制
## 5. 双写与缓存一致性
- [ ] 5.1 实现刷新接口双写逻辑:先删 Redis 缓存,再写 MySQL最后更新 Redis
- [ ] 5.2 实现双写顺序保证和错误处理MySQL 失败回滚Redis 失败记录日志)
- [ ] 5.3 实现缓存未命中回填逻辑TTL 30 天)
## 6. 接口改造
- [ ] 6.1 改造 `app/api/data.py` 中的行情数据读取接口,使用 `StorageManager`
- [ ] 6.2 改造 `app/api/data.py` 中的刷新接口,使用双写逻辑
- [ ] 6.3 改造 `app/services/collector.py`,使用 `StorageManager` 写入数据
## 7. 测试与验证
- [ ] 7.1 验证 Redis 缓存命中场景
- [ ] 7.2 验证 Redis 缓存未命中回源 MySQL 场景
- [ ] 7.3 验证 Redis 不可用降级到 MySQL 场景
- [ ] 7.4 验证 Redis 和 MySQL 均不可用降级到 SQLite 场景
- [ ] 7.5 验证刷新接口双写一致性
- [ ] 7.6 验证数据迁移完整性
```
## openspec/changes/storage-cache-refactor/specs/dual-write-consistency/spec.md
- Source: openspec/changes/storage-cache-refactor/specs/dual-write-consistency/spec.md
- Lines: 1-30
- SHA256: 6fd0c00433c73329adc93f97ea5a8b744d9a74148d7db40f30c843e92480b91e
```md
## ADDED Requirements
### Requirement: 刷新接口双写
系统应当在刷新接口中实现 Redis 和 MySQL 的同步双写,保证数据一致性。
#### Scenario: 刷新接口双写成功
- **WHEN** 用户调用刷新接口更新行情数据
- **THEN** 系统先删除 Redis 缓存,再写入 MySQL最后更新 Redis 缓存
#### Scenario: MySQL 写入失败
- **WHEN** 刷新接口写入 MySQL 失败
- **THEN** 系统返回错误响应,不更新 Redis 缓存,保证数据一致性
#### Scenario: Redis 写入失败
- **WHEN** 刷新接口写入 MySQL 成功但 Redis 写入失败
- **THEN** 系统返回成功响应MySQL 数据已持久化),输出 Redis 写入失败警告日志
### Requirement: 双写顺序保证
系统应当保证双写的顺序:先写 MySQL再更新 Redis。
#### Scenario: 双写顺序
- **WHEN** 刷新接口执行双写
- **THEN** 系统先写入 MySQL 并确认成功,再更新 Redis 缓存
### Requirement: 缓存回填一致性
系统应当在缓存未命中回填时保证回填数据与 MySQL 数据一致。
#### Scenario: 缓存未命中回填
- **WHEN** Redis 缓存未命中且从 MySQL 读取数据
- **THEN** 系统将 MySQL 数据回填到 RedisTTL 设置为 30 天
```
## openspec/changes/storage-cache-refactor/specs/mysql-persistence/spec.md
- Source: openspec/changes/storage-cache-refactor/specs/mysql-persistence/spec.md
- Lines: 1-41
- SHA256: f23680f51345a871dddbf3d18eacf6a53de1a20945d02516ffc55a2eb500af75
```md
## ADDED Requirements
### Requirement: MySQL 连接管理
系统应当提供 MySQL 连接管理能力,包括连接初始化、连接池管理和表结构初始化。
#### Scenario: MySQL 连接初始化成功
- **WHEN** 应用启动且 MySQL 配置有效
- **THEN** 系统成功建立 MySQL 连接并创建必要的表结构
#### Scenario: MySQL 连接初始化失败
- **WHEN** 应用启动但 MySQL 服务不可用
- **THEN** 系统输出错误日志并标记 MySQL 为不可用状态,但不阻止应用启动
### Requirement: 行情数据持久化存储
系统应当使用 MySQL 作为行情数据的主要持久化存储,替代 SQLite。
#### Scenario: 行情数据写入 MySQL
- **WHEN** 刷新接口接收到行情数据更新请求
- **THEN** 系统将数据写入 MySQL 的 `market_data`
#### Scenario: 行情数据从 MySQL 读取
- **WHEN** Redis 缓存未命中且 MySQL 可用
- **THEN** 系统从 MySQL 读取行情数据并返回
### Requirement: MySQL 表结构迁移
系统应当提供从 SQLite 到 MySQL 的数据迁移能力,保证历史数据不丢失。
#### Scenario: 首次启动数据迁移
- **WHEN** 应用首次启动且 MySQL 可用但表为空
- **THEN** 系统从 SQLite 读取历史数据并迁移到 MySQL
#### Scenario: 迁移完成
- **WHEN** 数据迁移完成
- **THEN** 系统输出迁移成功日志,后续读写直接操作 MySQL
### Requirement: MySQL 事务支持
系统应当使用 MySQL 事务保证数据写入的原子性和一致性。
#### Scenario: 批量写入事务
- **WHEN** 刷新接口需要写入多个品种的行情数据
- **THEN** 系统使用事务保证所有数据要么全部写入成功,要么全部回滚
```
## openspec/changes/storage-cache-refactor/specs/redis-cache-layer/spec.md
- Source: openspec/changes/storage-cache-refactor/specs/redis-cache-layer/spec.md
- Lines: 1-41
- SHA256: a0b5c7023c88339957ce5576ad326474fd0ab913756c223573766cbc9e23f761
```md
## ADDED Requirements
### Requirement: Redis 缓存连接管理
系统应当提供 Redis 连接管理能力,包括连接初始化、健康检查和连接池管理。
#### Scenario: Redis 连接初始化成功
- **WHEN** 应用启动且 Redis 配置有效
- **THEN** 系统成功建立 Redis 连接并输出初始化成功日志
#### Scenario: Redis 连接初始化失败
- **WHEN** 应用启动但 Redis 服务不可用
- **THEN** 系统输出警告日志并标记 Redis 为不可用状态,但不阻止应用启动
### Requirement: 行情数据缓存读取
系统应当优先从 Redis 读取行情数据,缓存未命中时回源 MySQL。
#### Scenario: Redis 缓存命中
- **WHEN** 用户请求行情数据且 Redis 中存在该数据
- **THEN** 系统直接从 Redis 返回数据,不访问 MySQL
#### Scenario: Redis 缓存未命中
- **WHEN** 用户请求行情数据但 Redis 中不存在该数据
- **THEN** 系统从 MySQL 读取数据,回填到 RedisTTL 30 天),并返回数据
### Requirement: Redis 缓存 TTL 管理
系统应当为缓存数据设置 30 天过期时间,过期后自动清理。
#### Scenario: 缓存数据过期
- **WHEN** 缓存数据超过 30 天未访问
- **THEN** Redis 自动清理该数据,下次访问时从 MySQL 重新加载
#### Scenario: 缓存数据续期
- **WHEN** 用户访问已缓存的行情数据
- **THEN** 系统返回缓存数据TTL 保持为 30 天(不续期)
### Requirement: Redis 缓存失效
系统应当在数据更新时主动失效相关缓存,保证数据一致性。
#### Scenario: 刷新接口触发缓存失效
- **WHEN** 用户调用刷新接口更新行情数据
- **THEN** 系统先删除 Redis 中对应的缓存数据,再执行双写流程
```
## openspec/changes/storage-cache-refactor/specs/storage-fallback/spec.md
- Source: openspec/changes/storage-cache-refactor/specs/storage-fallback/spec.md
- Lines: 1-38
- SHA256: feaf531250861c109bc48d06d684ff2520cb48269397211bdcefe7cda9013714
```md
## ADDED Requirements
### Requirement: 三级降级策略
系统应当实现 Redis → MySQL → SQLite 的三级降级读取策略,保证系统可用性。
#### Scenario: 正常模式Redis + MySQL 可用)
- **WHEN** Redis 和 MySQL 均可用
- **THEN** 系统优先从 Redis 读取,未命中时回源 MySQL
#### Scenario: Redis 不可用降级
- **WHEN** Redis 服务不可用但 MySQL 可用
- **THEN** 系统直接从 MySQL 读取数据,跳过 Redis 缓存层
#### Scenario: Redis 和 MySQL 均不可用降级
- **WHEN** Redis 和 MySQL 均不可用
- **THEN** 系统降级到 SQLite从 SQLite 读取数据并保证基础服务可用
### Requirement: 降级状态检测
系统应当能够检测存储后端的可用性状态,并在降级时输出明确的日志。
#### Scenario: 启动时降级检测
- **WHEN** 应用启动时检测到 Redis 或 MySQL 不可用
- **THEN** 系统输出降级警告日志,说明当前使用的存储模式
#### Scenario: 运行时降级触发
- **WHEN** 运行中 Redis 或 MySQL 连接异常
- **THEN** 系统自动切换到降级模式,输出降级日志,并继续提供服务
### Requirement: 降级恢复
系统应当能够在存储后端恢复后自动恢复到正常模式。
#### Scenario: Redis 恢复
- **WHEN** Redis 从不可用状态恢复
- **THEN** 系统自动恢复到 Redis + MySQL 模式,后续读取优先使用 Redis
#### Scenario: MySQL 恢复
- **WHEN** MySQL 从不可用状态恢复
- **THEN** 系统自动恢复到 Redis + MySQL 模式SQLite 降级结束
```

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-04

@ -0,0 +1,121 @@
## 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 一键部署
- **[取舍]** 同步双写性能略低于异步双写 → 换取数据一致性保证
- **[取舍]** 三级降级增加代码复杂度 → 换取系统高可用性

@ -0,0 +1,33 @@
## Why
当前系统使用 SQLite 作为唯一存储后端,无法满足行情数据高频读写的性能需求。每次刷新行情数据都需要直接读写磁盘文件,导致响应延迟高、并发能力受限。需要引入更高效的存储方案以提升系统吞吐量和用户体验。
## What Changes
- 引入 Redis 作为热数据缓存层,行情数据优先从 Redis 读取TTL 30 天)
- 引入 MySQL 作为持久化存储层,替代 SQLite 承担主要持久化职责
- 保留 SQLite 作为最终兜底方案,当 Redis 和 MySQL 均不可用时启用
- 新增数据访问层:实现 Redis → MySQL → SQLite 的三级降级读取策略
- 刷新接口改造:数据同步写入 Redis 和 MySQL保证双写一致性
- 启动流程改造:初始化 Redis/MySQL/SQLite 连接,检测可用性
## Capabilities
### New Capabilities
- `redis-cache-layer`: Redis 缓存读写能力包括热数据缓存、TTL 管理、缓存失效策略
- `mysql-persistence`: MySQL 持久化存储能力,替代 SQLite 承担主要持久化职责
- `storage-fallback`: 三级降级策略Redis → MySQL → SQLite保证系统可用性
- `dual-write-consistency`: 刷新接口双写能力,保证 Redis 和 MySQL 数据一致性
### Modified Capabilities
(无既有 spec 需要修改)
## Impact
- **代码**: `app/database.py`、`app/models.py`、`app/api/data.py`、`app/services/collector.py`、`app/main.py`
- **依赖**: 新增 `redis`、`pymysql`(或 `aiomysql`Python 包
- **配置**: `app/config.py` 新增 Redis/MySQL 连接配置
- **部署**: `docker-compose.yml` 新增 Redis 和 MySQL 服务
- **数据库**: 新增 MySQL 数据库初始化脚本,保留 SQLite 作为兜底

@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: 刷新接口双写
系统应当在刷新接口中实现 Redis 和 MySQL 的同步双写,保证数据一致性。
#### Scenario: 刷新接口双写成功
- **WHEN** 用户调用刷新接口更新行情数据
- **THEN** 系统先删除 Redis 缓存,再写入 MySQL最后更新 Redis 缓存
#### Scenario: MySQL 写入失败
- **WHEN** 刷新接口写入 MySQL 失败
- **THEN** 系统返回错误响应,不更新 Redis 缓存,保证数据一致性
#### Scenario: Redis 写入失败
- **WHEN** 刷新接口写入 MySQL 成功但 Redis 写入失败
- **THEN** 系统返回成功响应MySQL 数据已持久化),输出 Redis 写入失败警告日志
### Requirement: 双写顺序保证
系统应当保证双写的顺序:先写 MySQL再更新 Redis。
#### Scenario: 双写顺序
- **WHEN** 刷新接口执行双写
- **THEN** 系统先写入 MySQL 并确认成功,再更新 Redis 缓存
### Requirement: 缓存回填一致性
系统应当在缓存未命中回填时保证回填数据与 MySQL 数据一致。
#### Scenario: 缓存未命中回填
- **WHEN** Redis 缓存未命中且从 MySQL 读取数据
- **THEN** 系统将 MySQL 数据回填到 RedisTTL 设置为 30 天

@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: MySQL 连接管理
系统应当提供 MySQL 连接管理能力,包括连接初始化、连接池管理和表结构初始化。
#### Scenario: MySQL 连接初始化成功
- **WHEN** 应用启动且 MySQL 配置有效
- **THEN** 系统成功建立 MySQL 连接并创建必要的表结构
#### Scenario: MySQL 连接初始化失败
- **WHEN** 应用启动但 MySQL 服务不可用
- **THEN** 系统输出错误日志并标记 MySQL 为不可用状态,但不阻止应用启动
### Requirement: 行情数据持久化存储
系统应当使用 MySQL 作为行情数据的主要持久化存储,替代 SQLite。
#### Scenario: 行情数据写入 MySQL
- **WHEN** 刷新接口接收到行情数据更新请求
- **THEN** 系统将数据写入 MySQL 的 `market_data`
#### Scenario: 行情数据从 MySQL 读取
- **WHEN** Redis 缓存未命中且 MySQL 可用
- **THEN** 系统从 MySQL 读取行情数据并返回
### Requirement: MySQL 表结构迁移
系统应当提供从 SQLite 到 MySQL 的数据迁移能力,保证历史数据不丢失。
#### Scenario: 首次启动数据迁移
- **WHEN** 应用首次启动且 MySQL 可用但表为空
- **THEN** 系统从 SQLite 读取历史数据并迁移到 MySQL
#### Scenario: 迁移完成
- **WHEN** 数据迁移完成
- **THEN** 系统输出迁移成功日志,后续读写直接操作 MySQL
### Requirement: MySQL 事务支持
系统应当使用 MySQL 事务保证数据写入的原子性和一致性。
#### Scenario: 批量写入事务
- **WHEN** 刷新接口需要写入多个品种的行情数据
- **THEN** 系统使用事务保证所有数据要么全部写入成功,要么全部回滚

@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: Redis 缓存连接管理
系统应当提供 Redis 连接管理能力,包括连接初始化、健康检查和连接池管理。
#### Scenario: Redis 连接初始化成功
- **WHEN** 应用启动且 Redis 配置有效
- **THEN** 系统成功建立 Redis 连接并输出初始化成功日志
#### Scenario: Redis 连接初始化失败
- **WHEN** 应用启动但 Redis 服务不可用
- **THEN** 系统输出警告日志并标记 Redis 为不可用状态,但不阻止应用启动
### Requirement: 行情数据缓存读取
系统应当优先从 Redis 读取行情数据,缓存未命中时回源 MySQL。
#### Scenario: Redis 缓存命中
- **WHEN** 用户请求行情数据且 Redis 中存在该数据
- **THEN** 系统直接从 Redis 返回数据,不访问 MySQL
#### Scenario: Redis 缓存未命中
- **WHEN** 用户请求行情数据但 Redis 中不存在该数据
- **THEN** 系统从 MySQL 读取数据,回填到 RedisTTL 30 天),并返回数据
### Requirement: Redis 缓存 TTL 管理
系统应当为缓存数据设置 30 天过期时间,过期后自动清理。
#### Scenario: 缓存数据过期
- **WHEN** 缓存数据超过 30 天未访问
- **THEN** Redis 自动清理该数据,下次访问时从 MySQL 重新加载
#### Scenario: 缓存数据续期
- **WHEN** 用户访问已缓存的行情数据
- **THEN** 系统返回缓存数据TTL 保持为 30 天(不续期)
### Requirement: Redis 缓存失效
系统应当在数据更新时主动失效相关缓存,保证数据一致性。
#### Scenario: 刷新接口触发缓存失效
- **WHEN** 用户调用刷新接口更新行情数据
- **THEN** 系统先删除 Redis 中对应的缓存数据,再执行双写流程

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: 三级降级策略
系统应当实现 Redis → MySQL → SQLite 的三级降级读取策略,保证系统可用性。
#### Scenario: 正常模式Redis + MySQL 可用)
- **WHEN** Redis 和 MySQL 均可用
- **THEN** 系统优先从 Redis 读取,未命中时回源 MySQL
#### Scenario: Redis 不可用降级
- **WHEN** Redis 服务不可用但 MySQL 可用
- **THEN** 系统直接从 MySQL 读取数据,跳过 Redis 缓存层
#### Scenario: Redis 和 MySQL 均不可用降级
- **WHEN** Redis 和 MySQL 均不可用
- **THEN** 系统降级到 SQLite从 SQLite 读取数据并保证基础服务可用
### Requirement: 降级状态检测
系统应当能够检测存储后端的可用性状态,并在降级时输出明确的日志。
#### Scenario: 启动时降级检测
- **WHEN** 应用启动时检测到 Redis 或 MySQL 不可用
- **THEN** 系统输出降级警告日志,说明当前使用的存储模式
#### Scenario: 运行时降级触发
- **WHEN** 运行中 Redis 或 MySQL 连接异常
- **THEN** 系统自动切换到降级模式,输出降级日志,并继续提供服务
### Requirement: 降级恢复
系统应当能够在存储后端恢复后自动恢复到正常模式。
#### Scenario: Redis 恢复
- **WHEN** Redis 从不可用状态恢复
- **THEN** 系统自动恢复到 Redis + MySQL 模式,后续读取优先使用 Redis
#### Scenario: MySQL 恢复
- **WHEN** MySQL 从不可用状态恢复
- **THEN** 系统自动恢复到 Redis + MySQL 模式SQLite 降级结束

@ -0,0 +1,46 @@
## 1. 依赖与配置
- [ ] 1.1 添加 `redis`、`pymysql` 依赖到 `requirements.txt`
- [ ] 1.2 在 `app/config.py` 中新增 Redis 和 MySQL 连接配置项host、port、user、password、database
- [ ] 1.3 在 `docker-compose.yml` 中新增 Redis 和 MySQL 服务定义
## 2. 数据库模型与初始化
- [ ] 2.1 创建 `app/mysql_database.py`,实现 MySQL 连接引擎和 SessionLocal
- [ ] 2.2 创建 `app/redis_client.py`,实现 Redis 连接池和客户端封装
- [ ] 2.3 在 `app/main.py``lifespan` 中初始化 Redis 和 MySQL 连接,检测可用性
- [ ] 2.4 创建 MySQL 表结构初始化脚本(复用现有 ORM 模型)
## 3. 数据迁移
- [ ] 3.1 创建数据迁移脚本,从 SQLite 读取历史数据并写入 MySQL
- [ ] 3.2 在应用启动时检测 MySQL 表是否为空,自动触发迁移
## 4. 存储管理层
- [ ] 4.1 创建 `app/storage_manager.py`,实现 `StorageManager` 抽象层
- [ ] 4.2 实现 Redis 缓存读取逻辑(命中返回,未命中回源 MySQL 并回填)
- [ ] 4.3 实现 MySQL 持久化读写逻辑
- [ ] 4.4 实现三级降级策略Redis → MySQL → SQLite
- [ ] 4.5 实现降级状态检测和恢复机制
## 5. 双写与缓存一致性
- [ ] 5.1 实现刷新接口双写逻辑:先删 Redis 缓存,再写 MySQL最后更新 Redis
- [ ] 5.2 实现双写顺序保证和错误处理MySQL 失败回滚Redis 失败记录日志)
- [ ] 5.3 实现缓存未命中回填逻辑TTL 30 天)
## 6. 接口改造
- [ ] 6.1 改造 `app/api/data.py` 中的行情数据读取接口,使用 `StorageManager`
- [ ] 6.2 改造 `app/api/data.py` 中的刷新接口,使用双写逻辑
- [ ] 6.3 改造 `app/services/collector.py`,使用 `StorageManager` 写入数据
## 7. 测试与验证
- [ ] 7.1 验证 Redis 缓存命中场景
- [ ] 7.2 验证 Redis 缓存未命中回源 MySQL 场景
- [ ] 7.3 验证 Redis 不可用降级到 MySQL 场景
- [ ] 7.4 验证 Redis 和 MySQL 均不可用降级到 SQLite 场景
- [ ] 7.5 验证刷新接口双写一致性
- [ ] 7.6 验证数据迁移完整性
Loading…
Cancel
Save