行情数据落盘与内存缓存的层级设计,核心答案是:分层架构,各司其职,热数据住内存,冷数据住磁盘,中间用同步策略保证不丢不重。这套设计直接决定了量化策略的成交质量与回测精度,别指望单靠一套Redis或者一款时序数据库就能通吃所有场景,真正的瓶颈往往藏在数据从内存刷到磁盘的那一瞬间。
行情数据落盘方案对比:写放大与查询效率的取舍
选落盘方案前,得先搞清楚行情数据的脾气,它不是普通的业务数据,而是典型的时序数据,特点是写多读少、按时间顺序追加、几乎不做单点修改,多数情况下,行情数据落盘方案对比绕不开三兄弟:关系型数据库、时序数据库、文件系统。
| 方案 | 写入速度 | 压缩比 | 典型场景 | 主要痛点 |
|---|---|---|---|---|
| MySQL/PostgreSQL | 慢 | 低 | 日线、分钟线归档 | 写放大严重,历史分区维护繁琐 |
| ClickHouse/TDengine | 极快 | 高 | Tick级回测、实时分析 | 集群运维有门槛,小规模部署不划算 |
| Parquet/ORC文件 | 中 | 极高 | 冷数据存储、离线计算 | 查询需要额外引擎,不支持点查 |
拿MySQL来说,如果硬扛每秒几千条的Tick数据写入,磁盘IO很快就会成为瓶颈,而且行式存储的压缩率惨不忍睹,一年下来几个TB不是开玩笑,业界共识是,MySQL这类关系型库只适合放日线以上的低频数据,用来做最终对账和审计,而高频TICK数据,要么直接上ClickHouse这类列式存储,要么就用Parquet文件按天分区扔到对象存储里。
这里有个实操细节:即便选择文件系统,也别裸写文件,建议用二分查找索引文件加数据文件分离的模式,每个交易日生成一个数据文件,索引文件单独维护时间戳偏移量,查询某一段行情时,先二分查索引,再顺序读数据文件,秒级定位到毫秒级的数据段,这套方案吞吐量远超数据库,成本也最低,据行业共识,顶尖量化私募的行情存档,占比最大的一定是列式压缩文件,而不是数据库。
行情数据内存缓存设计:分级缓存撑起低延迟查询
落盘解决的是“存得住”,内存缓存解决的是“查得快”,行情数据内存缓存怎么设计,核心思路是分级降噪,逐层过滤,行情数据的访问热度分布极不均匀,最近一分钟的热点数据占据了绝大多数查询请求,而历史高频数据的访问频率呈指数下降,单层缓存结构要么浪费内存,要么命中率低得可怜。
一级缓存:实时行情订阅通道
这一层直接对接交易所或券商行情源,数据以事件驱动的方式推送到本地内存,它面向的是实盘交易模块,要求极致的吞吐和微秒级的延迟,这一层的数据结构通常设计为环形缓冲区(Ring Buffer),每个标的占据固定大小的槽位,新数据覆盖旧数据,为什么不用Map?因为Map在高并发写入时会触发哈希冲突和扩容,延迟抖动不可控,环形缓冲区写入是纯内存操作,配合无锁队列,在主流配置下,单线程延迟能够稳定控制在微秒级别。
二级缓存:热点窗口数据库
一级缓存太“短命”,数据几秒钟就被覆盖了,二级缓存要解决的是分钟级和小时级的历史回看,通常使用Redis或者更轻量的内存数据库,存储最近N个交易日的数据,这里有个关键选择:数据格式。Redis存行情,别用String存JSON,那是在浪费内存和反序列化时间,更专业的做法是用Hash结构存储Bar数据,用Sorted Set存储Tick序列,并开启RDB和AOF混合持久化,如果不做持久化,一旦Redis宕机,几分钟内的数据直接蒸发。
三级缓存:本地磁盘映射
二级缓存之外,是基于内存映射文件(MMAP)的本地缓存,MMAP的好处是,它像内存一样访问,但背后是磁盘文件,当二级缓存未命中时,查询直接通过内存映射加载对应日期分片的数据,这个过程没有用户态和内核态的切换拷贝,比传统的read/write系统调用快一个量级,多数量化框架的“历史数据自动补全”功能,底层就是靠MMAP实现的。
数据一致性:缓存与磁盘之间如何保证不丢不乱
缓存和落盘之间最怕两件事:丢数据和乱顺序,实战中,一套标准的写入链路应该是:行情播报进来 → 写共享内存(WAL) → 内存数据结构更新 → 异步批量落盘。
这里WAL(Write-Ahead Logging)不是数据库专利,行情系统同样需要,行业专家指出,事务日志的顺序写性能远超随机写,这是事物固有的物理规律,当行情线程收到一笔Tick时,先把这一条数据顺序追加到WAL日志文件尾部,WAL只要写完,这笔数据就算“安全落盘”了,即使下一秒停电,重启后也能从WAL完整恢复,之后,后台线程每隔几百毫秒,把内存中的最新数据批量刷入时序数据库或Parquet文件,刷成功后再截断WAL。
这套机制在“双写”之外,还隐含了一个技术细节:写入去重,网络重连或重启补数时,很可能收到重复的数据包,落盘时必须以 「交易所时间戳 + 行情序号」 作为唯一键,用幂等写入覆盖旧数据,而不是简单追加,否则回测数据里出现一根影线超级长的假K线,策略绩效直接失真。
针对不同级别的数据,容错策略要分开粒度:
- 高频Tick/逐笔委托:允许极少量丢失,优先保证实时性,采用异步批量刷盘。
- 分钟级/日级Bar:必须百分百可靠,采用同步刷盘,每个Bar周期结束立即写入并确认。
- 合约基础信息(合约乘数、保证金率):改动频率极低,写入数据库时需做全量快照备份,因为这类数据出错会导致所有策略连带出错。
缓存与磁盘的版本对齐问题经常被忽视,但这实际上是一个关键问题,内存中维护着一个数据版本号(如2026-12-30-15:00:00),磁盘中同样维护一个,当后台线程完成一次完整刷盘并更新磁盘版本号后,清理缓存与磁盘之间的过期索引,每次策略启动加载数据时,先比对两个版本号,如果缓存版本领先,直接加载缓存;如果落后,说明脏数据残留,先清空缓存再从磁盘回放WAL恢复。
性能调优实践:从数值参数到底层IO的排查路径
架构定好了,真正折磨人的是性能压测,很多时候,行情数据落盘与内存缓存的层级设计在理论上无懈可击,但一跑实盘延迟就飙升,问题通常出在几个被忽视的角落。
别让业务线程直接碰磁盘,如果发现CPU占满的不是策略计算模块,而是磁盘IO等待,多半是在主链路里混入了同步写操作,解决路径:把落盘动作抽象成独立的IO线程池,将行情数据从生产线程通过有界队列投递给IO线程,队列满了怎么办?直接丢弃最老的未落盘数据,并累加一个丢弃计数器,宁可少几笔数据,也不能阻塞当前实盘。
内核参数要调,大多数人安装了数据库就指望默认配置能扛住高并发,这在行情场景下行不通,像网络中断合并、socket缓冲区大小、文件描述符上限以及脏页回写间隔,都会直接影响吞吐,特别是/proc/sys/vm/dirty_ratio和dirty_expire_centisecs,如果脏页回写太频繁,会瞬间抢走磁盘带宽,导致锁等待。
存储硬件也要区分对待:
- WAL日志:放单独的NVMe SSD或傲腾持久化内存,不用考虑容量,一块几百GB的盘就够了,目的是极致的低延迟。
- 时序数据库:放大容量SATA SSD组成的RAID,看重顺序读写带宽和压缩率。
- 冷数据对象存储:放HDD或云上的低频存储,反正访问频率极低,图个便宜和安全。
内存垃圾回收在Java体系里是个大坑,在C++里同样不能掉以轻心,如果你用Go或Java写行情网关,
堆外内存是个值得考虑的技术路线,堆外内存不受GC暂停影响,但需要手动管理生命周期,用不好会有内存泄漏风险,较为保守的做法是:预分配大块DirectByteBuffer池,复用对象,避免高频创建短命对象。
行情数据落盘方案对比的选型建议
回到开篇那句话:没有银弹,只有适配。行情数据落盘与内存缓存的层级设计,最终要看你服务的核心场景。
若你的目标是低延迟实盘交易,内存缓存设计的优先级远高于落盘,因为磁盘那一层只是用来事后分析的,门槛不高,只要WAL不丢失,延迟的命脉全在缓存层,把更多精力花在优化一级缓存的无锁化和二级缓存的命中率上。
若你的目标是策略回测和科研分析,行情数据落盘方案对比的结果就变得至关重要,回测引擎需要反复扫描海量历史数据,此时数据的压缩率和查询性能决定了你的研究效率,建议直接把数据落地为Parquet文件,并用ClickHouse或DuckDB做查询加速层,尽量不要用MySQL去存Tick数据,这样能避免不必要的性能瓶颈。
一个好的架构不会一步到位,先跑通“内存接收 + WAL + 异步队列 + 本地文件”的最小闭环,观察单标的的写吞吐和查询P99延迟,再决定是否需要引入分布式消息队列(如Kafka)来削峰填谷,当单节点瓶颈到来时,再按标的或交易日期做分片,先把垂直方向做扎实。
常见问题解答
这部分整理几个被问得最多的点,也是架构落地时最容易踩坑的地方。
行情数据到底先写内存还是先写磁盘?
先写WAL(磁盘顺序日志),再更新内存索引。 顺序写磁盘极快,远低于随机写,先保证数据持久化不丢,再服务查询请求,既然写WAL已经完成而且还挺快,就没必要为了速度牺牲可靠性。
为什么行业里越来越多人用Parquet存历史行情?
因为列式存储的压缩率惊人,Tick数据里的时间戳、价格、成交量字段,相邻行之间的差异极小,列式压缩能达到10倍以上的压缩比,原生文件格式查询速度快;同时分区裁剪能力也使得处理某个时间窗口的数据时能像按索引查找那样只读必要的数据块。
内存缓存一般要设置多大的过期时间?
不要设置统一的过期时间。一级缓存是环形覆盖,新的推挤旧的,没有过期概念,二级缓存建议保留最近7个交易日的完整数据,超过7天的数据已经不具备高频实时访问的热度,浪费内存,按需从磁盘加载反而更划算。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/631330.html





