连接池与缓存¶
这个项目相比同类实现快 约 2 倍,来源就是这两块。
缓存¶
两级结构¶
- 内存层:进程内 LRU,命中就是字典查找
- 磁盘层:单文件 SQLite,开 WAL 模式,可多进程共享
- 未传
cache_dir时只有内存层(进程退出即失效)
TTL 分级¶
不同数据的「新鲜度要求」差了几个数量级,所以 TTL 是分级的:
| 数据类型 | TTL | 理由 |
|---|---|---|
| 五档报价 | 5s | 变化最快 |
| 分钟 K 线 | 60s | 边界处才会新增一根 |
| 日 K 线 | 当天 | 当天数据在盘中会变 |
| 复权 K 线 | 1 天 | 复权因子一天最多变一次 |
| 历史逐笔 | 永久 | 已经成交的不会变 |
| 证券列表 | 1 天 | 只有新股上市才变 |
缓存的是「已解析记录」¶
缓存里存的是解析好的 dataclass 对象(codec/payload.py 做 pickle + zlib),
不是原始响应字节。所以缓存命中时是 0 网络 + 0 解析,只有一次反序列化。
这也是为什么重复请求能从 0.31s 降到 0.016s。
single-flight¶
同一时刻对同一个缓存键的并发请求会自动合并:第一个去回源,其余的等待并共享结果。
# 100 个协程同时请求同一只股票的同一段 K 线
# → 只发 1 次网络请求
results = await asyncio.gather(*[
td.get_bars("sh600000", KlinePeriod.DAY, count=800)
for _ in range(100)
])
上限由 max_inflight(默认 64)控制。
缓存键会做规范化——
get_quotes的键是排序去重后的股票集合, 所以[A, B]和[B, A]共享缓存。
连接池¶
动态扩缩容¶
| 参数 | 默认 | 含义 |
|---|---|---|
pool_min |
3 |
保底连接数 |
pool_max |
12 |
上限 |
mac_pool_cap |
4 |
单台 MAC 服务器连接上限 |
timeout |
3.0 |
连接超时(秒) |
按等待队列长度与空闲时长动态扩缩容,不是启动时就拉满。
健康分与选路¶
每台服务器维护一个健康分,取连接时按健康分 + EWMA 延迟综合选路:
| 事件 | 健康分变化 |
|---|---|
| 请求失败 | × 0.5 |
| 请求成功 | + 0.2 |
| 连续失败 | 进入 120s 冷却,冷却期内不再被选中 |
失败会跨服务器故障转移——重试用的是另一台,而不是同一台重连。
EWMA(指数加权移动平均)延迟让选路对「最近变慢」敏感,而不是被历史均值拖住。
MAC 通道的单台连接上限¶
mac_pool_cap 默认 4,与标准池的 std_pool_cap 一致——这是对端单台连接限制,
不是本项目的选择。两个通道的可用台数见 通道与数据源:
实测可达台数会随服务器上下线变化,以当日测速缓存为准。
并发分页¶
大请求会被拆成多页,每页带显式 start 偏移——无状态,所以可以并行。
多连接并行拉取 → 按序拼接 → 某页失败就换连接重试该页。
两种例外:
| 场景 | 策略 | 原因 |
|---|---|---|
KlinePeriod.MIN_1 |
单连接串行 | 各服务器历史深度不一致,并发会导致非确定性空洞 |
| MAC 通道复权 K 线 | 单连接串行(_fetch_mac_kline_serial) |
同上 |
调参建议
concurrency 超过 pool_max 之后只会排队。实践值 8~16。
MAC 类请求(逐笔 / 分时)并发能力受 mac_pool_cap 限制,调大 concurrency 无用。