跳到主要内容

缓存策略

三层缓存体系​

L1: 内存缓存(cache_manager)​

基于 TTL 的进程内缓存,交易时段短缓存保证数据新鲜度(market_data_service/infra.py 的 TTL_TRADING/TTL_IDLE 代理,数值来自 core/config.py,可经环境变量覆盖):

_CACHE_TTL_TRADING = {
"sentiment": 10, # 指数/涨跌家数(秒级变化)
"distribution": 10, # 涨跌幅分布直方图
"fund_flow": 15, # 资金流概况(中频变化)
"industry_rank": 30, # 行业排名(低频变化)
"hot_stocks": 10, # 热门股票排行
"sector_treemap": 15, # 板块热力图
}
# 休市:统一 CACHE_TTL_IDLE = 3600(1 小时)

交易时间判定​

def _is_trading_hours() -> bool:
"""A股交易时间:周一至周五 9:30-11:30, 13:00-15:00"""
now = datetime.now()
if now.weekday() >= 5:
return False
hm = now.hour * 100 + now.minute
return (930 <= hm <= 1130) or (1300 <= hm <= 1500)

分钟线轮询缓存(IntradayCache)​

分钟 K 线(/api/v1/stocks/{code}/intraday)独立于 L1,使用 IntradayCache(market_data_service/intraday_cache.py)应对前端 30s 高频轮询,避免并发请求穿透到数据源:

  • TTL 映射:高频周期 1m/5m = 30s(与前端轮询对齐),15m=900s、30m=1800s、60m=3600s;动态 TTL——盘中用周期基准,盘后统一放宽 ≥3600s(数据不再变化,长时间可复用)
  • 新鲜度语义:get() 返回 (records, fresh)——fresh 命中直接返回;过期(stale)仍返回旧数据供前端展示,同时后台线程触发回源刷新并写回(stocks.py::_spawn_intraday_refresh 按 key 防重入),保证轮询窗口内无空窗、且陈旧数据不会"陈旧永驻"
  • 跨 TTL 去重合并:set() 按 datetime 合并新老 bar,同时间点新数据覆盖旧数据,防止追加轮询数据时重复
  • 响应头:stocks.py 对 intraday 响应统一返回 Cache-Control: public, max-age=30(盘中)/ max-age=3600(盘后,is_trading_hours() 动态判定)+ 对应的 X-Cache-TTL,source 字段区分 cache(新鲜命中)/cache_stale(过期复用)/live(实时回源)
  • 失效:invalidate() 支持精确(code+period)/按 code /全清,供后台同步任务主动清缓存

L2: DB 快照持久化​

成功获取的数据通过 fox 门面写入持久层(market_snapshot 通用快照表已于 2026-08-22 退役迁移):

  • fund_flow:UPSERT 到 fox_market_daily(盘后定型权威数据)
  • 其余 5 类(sentiment/distribution/industry_rank/hot_stocks/sector_treemap):UPSERT 到 fox_market_snapshot(date+data_type 双主键,当日每类一行,幂等)
  • 样本估算数据不落库(sample_based 标记),避免休市时回退到不具代表性的样本
  • 当日数据快速读取(休市时避免无谓外部调用);历史数据兜底取最近一条有效记录(带 _snapshot_date/_source: "db_history" 标注)
  • 过期数据保护(fox_market_data._snapshot_stale):历史兜底快照须不早于「最近已定型交易日 − STALE_MAX_AGE(3 天)」,否则返回 None 由上层输出空结构——按交易日而非自然日衡量,长假不误杀,同步停更超窗才拦截

L3: 静态数据 lru_cache​

参考 AkShare @lru_cache() 模式,进程内永不变的参考数据使用 functools.lru_cache(market_data_service/infra.py):

@lru_cache(maxsize=1)
def index_code_map() -> dict[str, str]:
"""主要指数代码→名称映射(18 条:A股 14 + 港美 4)"""
return {"000001": "上证指数", "399001": "深证成指", ...}

@lru_cache(maxsize=1)
def index_tencent_codes() -> dict[str, str]:
"""指数代码→腾讯前缀代码(sh/sz/bj/hk/us,显式市场不可猜测)"""

@lru_cache(maxsize=1)
def breadth_sample_codes() -> tuple[str, ...]:
"""涨跌分布抽样50只代表性股票"""
return ("600519", "601318", ...)

@lru_cache(maxsize=1)
def em_all_market_fs() -> str:
"""东财全市场A股筛选条件"""
return "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"

融合层缓存(fox_engine)​

融合层读路径(fox_engine/engine.py,FoxDataEngine)内置分级 TTL 进程缓存,按数据变化频率分档:

缓存TTL数据
_VALUATION_TTL300s估值快照(随日行情变化)
_PROFILE_TTL / _FINANCE_TTL600s股票主数据 / 财务指标(低频变化)
_EVENT_TTL1800s龙虎榜 / 大宗 / 两融 / 公告 / 新闻
_STATIC_TTL3600s行业 / 指数日线 / K线 / 分红 / 股东户数序列
  • 失效:fox.invalidate(code=None) 全清 / 指定 code 精确清除;ETL 写路径(writer sync_*)完成后调用,保证读路径不返回脏数据
  • 市场级:fox_market_data.py 盘后数据(fox_market_daily)另设 4h 进程缓存(_EOD_CACHE_TTL=14400),persist_daily() 写入成功即失效
  • 消费方统一经 fox_engine 门面单例(fox)访问,避免各模块自建引擎实例导致缓存分叉

聚合端点缓存(services/abnormal_moves.py)​

/realtime/abnormal-moves 聚合三个变化频率差异极大的子源,而前端以 tier="realtime"(15s)轮询。若三个子源用同一节奏重算,绝大多数算出来的是同一个值 —— 实测单次 3.3s、20 次 TDX TCP 往返,15s 轮询 = 4800 次往返/小时/客户端。 故按「子源真实变化频率」分档,并配 single-flight(同 key 并发只跑一次 producer):

子源变化频率TTL
auction(竞价窗口 09:15–09:30 内)实时20s
auction(窗口外)全字段冻结(见下)1800s
intraday(东财 push2ex)真·实时20s
deviation(龙虎榜)盘后发布,日级1800s
任一子源的空结果—20s(_TTL_EMPTY)

三条设计约束(都是踩过坑才加的):

  1. 空结果用独立短 TTL:既避免一次瞬时失败污染长窗口(龙虎榜窗口 30min, 会表现为「异动一直是空的」且不报错),也避免「本来就没数据」的正常态 每轮重打数据源(实测空态仍耗 1.9s/次)。
  2. 窗口状态进 key:否则 09:14 写入的长 TTL 条目会被顶进 09:15–09:30 这段最需要实时的窗口。非「交易日且 ≥ 09:15」时另用独立 key,与 live/frozen 完全隔离。
  3. 不用 core.cache_manager:那是文件级缓存,为「重启后仍可用」而设计; 此处 TTL 最长 30min 且是盘中易变数据,落盘只带来磁盘 churn 无收益。

auction 窗口外能给到 1800s,是因为池子取昨日涨停池(当日不再变化)+ 撮合 09:25 落定 + 昨收固定 —— 三项都固定,重算结果必然相同。 另:非交易日 / 交易日 09:15 前根本不触 TDX(见 source-tdx.md 的 get_auction 注意事项),故实际往返降到约 40 次/小时。

缓存失效策略​

场景行为
TTL 到期自动失效,下次请求重新获取
数据无效不写入缓存(builders.py::is_valid_data 校验)
东财被封不缓存空数据,触发降级(TDX → 腾讯 → 新浪 → DB 历史)
样本估算数据不写入 DB 快照(sample_based 标记)
进程重启L1 全部失效,从 L2/L3 恢复

设计原则​

核心原则
  • 不缓存失败:外部 API 返回空/异常时不写入缓存,确保下次可重试
  • 不缓存过期:DB 历史兜底快照超出陈旧窗口(_snapshot_stale,最近交易日 − 3 天)直接判空,不冒充当日数据
  • 交易时段敏感:指数/涨跌家数/热门榜 10s、资金流/热力图 15s、行业排名 30s

相关文档​