剖析⼀个⽤C++写的⾏情交易系统
最近hen ci hen ci⽤C++写完了⼀整套证券⾏情系统,但是不是服务沪深交易所的,是给⽂交所⽤的。整个系统涵盖了从DBF⽂件解析开始到客户端展现这⼀整条逻辑。想来⼀年多没有更新博客了,所以趁这个机会,把整个系统的架构和开发中遇到的问题写下来,权当总结和分享。
⾸先要说明的是,整个系统的架构都是以当前业务为出发点的,所以和⽬前⽹上看到的,⽐⽅说⼴发⾃研的系统是肯定有差别的,我们就没有合规⼀说。另外,从⽤户规模和市场活跃程度来看,我们也⽆法和国内证券市场⽐较,所以和⽬前公开出来的系统结构相⽐也还是有差异的。我们根据⾃⾝⼈⼒资源限制和当前业务⾓度考虑,⾸要⽬标是希望整体架构要简单,易于横向扩展。因为⼀,⼈太少,平均下来,我就俩⼈;⼆,不懂业务,其中接⼿我服务端开发的还是应届毕业⽣。
这⾥先把系统结构图罗列⼀下:
你会看到⾥⾯有很多让⼈意想不到的东西,⽐⽅说SQLite!
容我后续慢慢来说!
DBF⽂件解析
⽬前沪深L1数据更新的频率是3秒。⽂交所这边是1秒。总得来说解析DBF⽂件没有太⼤的难度,就是要理解⽂件结构。DBF⽂件结构其实也是开放的,随便查。这个程序没有任何难度,不需要多线程,只有⼀个要求,就是解析⽂件越快越好。
关于DBF么,我就画⼀个结构图在这⾥好了。⽅便⼤家查阅。session数据错误是什么意思
⾏情数据库程序
⾸先来张图展⽰下⾏情数据库程序的结构。
从图上看,我们的⾏情程序分三⼤模块:
1. 业务驱动对象
2. Logger
3. Config Agent
Logger
也就是⽇志。我们这⾥是直接⽤了Linux⾃带的Syslog。当然了回归到代码的话,是⼀个logger接⼝,然后在Linux上基于Syslog实现了这个logger接⼝。
为什么选⽤Syslog?⼀是我们没有那么多资源搞⼀个异步logger;⼆是以我们⽬前的压⼒来看,Syslog从各⽅⾯都满⾜我们的要求。⽽且是独⽴的进程,万⼀发⽣不测,不影响我们⾏情正常运⾏,⽆⾮就是没有⽇志了。
Config Agent
配置⽂件读取对象。好像没有神马好说的。
业务驱动对象
我们重点来说下这个业务驱动对象。
⾸先来说⼀说这个业务驱动对象到底是⽤来⼲啥的。
服务端程序在设计的时候,你肯定会将业务进⾏层次划分,这样做的好处就是结构清晰,易于维护。每⼀层就是⼀个模块,模块之间规定好访问接⼝,形象地说就是⾼内聚低耦合。
在我们这⾥,模块之间的接⼝都是以boost:: signals2::signal来做的。
⽹络层
⽹络层只开放了5个接⼝:
其中shield是指DBF⽂件解析器。当连上了DBF解析器的时候,⽹络层会向上层发送⼀个shield_connected_signal_t类型的信号,⼀般这个信号上⼀层都没有订阅。当DBF解析器推送数据过来的时候,⽹络层解析成功后,就会发送⼀个shield_data_ready_signal_t信号。这个信号上次肯定会订阅的,不然程序就不⽤跑了。
另外,cache打头的信号指的是从缓存程序发送过来的请求。和DBF的shield类似,基本上⼀⽬了然,顾名思义。
数据转换层
这层其实可有可⽆。这层的作⽤就是将⽹络过来的⼆进制数据解码成protobuf message对象,然后将protobuf message对象解码成⽆第三⽅库依赖的本地数据包,并根据数据包的类型,发送相应的信号给数据层。之所以有⼀个protobuf到本地包的转换,主要是感谢Google,毕竟Google是出了名的喜欢弃坑。或者说,等哪天有了更好的数据包⼆进制序列号反序列化库,只需要考虑将这⼀场替换掉,就万事⼤吉了。当然,⽬前来看,这⼀层肯定是我想多了!
数据层
数据层也就是我们真正处理业务的模块。这个模块的特点是,宏观上看简单,微观上看复杂。
宏观上看⽆⾮就三件事情:
1. 从DBF数据计算出各种周期⾏情数据
2. 将最新的⾏情数据存盘
3. 将最新的⾏情数据推送到前端缓存
微观上复杂怎么说呢?复杂就复杂在计算周期⾏情数据。
⾏情数据计算
我们给每⼀个⾏情数据都指定了⼀个数据项ID。⽐⽅说开盘价我们可以⽤0xFFFFFFFF这个ID来表⽰,收盘价可以⽤0xFFFFFFFE来表⽰。
除此以外,我们还具体定义了周期ID。实时周期,⼀分钟周期,五分钟周期,⼗五分钟周期,三⼗分钟周期,六⼗分钟周期,⽇周期,周⽉季年周期等。
DBF⾥的数据就相当与实时周期数据。
所以我们有多少数据要计算?显⽽易见,数据项ID个数x周期个数!
数据项ID说实话,并不少!所以计算周期数据真的是相当的重体⼒!
那么计算⾏情数据到底应该是:
1. 遍历每⼀个数据项,计算出这个数据项所有周期的数据
2. 还是先根据周期来,计算出每⼀个周期下所有数据项的值
这个话题说到这⾥,感觉说不下去了。因为我最后付诸的⾏动不是这么搞的。我不确定我的算法是不是最优算法,但是我想应该⼋九不离⼗?
要把这个想法说清楚,估计还是要真正写⼀把,你才知道到前⾯提到的说法到底对不对。
我前前后后写了⼤概⾄少两遍。我最终的想法容我下来吗慢慢说来。
先说⼀说我们数据的特点。⾏情数据其实都是以时间为顺序的离散数据点!这个能理解吧?所以,我们的⾏情数据在根据DBF⽂件计算的时候对于任意⼀个周期来说,⽆⾮遇到两种情况:
1. 更新最新的这个时间点数据
2. ⽣成⼀个最新时间点的数据
另外,在这两个⼤类的情下还要考虑⼀个因素:
更新当前最新数据点的时候是否和该周期前⼀个数据点有依赖关系?
⽣成⼀个最新数据点的时候是否和该周期前⼀个数据点有依赖关系?
所以我在计算⾏情数据时,先区分是否⽣成⼀个新的数据点。然后根据周期来计算的。
这个代码注释可能不对,⼼领神会就好。
关于⾏情计算这⾥再说最后⼀点,前段时间发现⼀个⼋哥。如果DBF⽂件⾥的某只代码/某个证券的昨收为0时,这个时候,这只代码当天的昨收是0还是其他值?正确答案是,不是零,是上⼀个交易⽇的昨收。
数据存盘
终于说到数据存盘,说到SQLite了。
先不从业务⾓度考虑这个问题。当有⼤量数据要存储的时候,⼀般的解决⽅案是什么?没有特殊要求的情况下,多数会使⽤数据库来解决这个问题。毕竟⾃研⼀套数据存储⼯具,⽽且要做得好,并不是⼀件
简单的事情。接着我说⼀说同花顺和恒⽣,据我了解同花顺和恒⽣都是⾃⼰设计的⼆进制⽂件格式来保存⾏情数据的(不要问我怎么知道的,毕竟当初我是同花顺的)。恒⽣的不知道,同花顺我是有切⾝体会的。这个⼆进制⽂件经常会莫名其妙写挂了。所以现在经典的统⼀版客户端出现数据错误的时候,多半是⽂件已经写坏了。你需要“重新初始化”!
基于上⾯的考虑,我果断选择了SQLite。好处就是事务性,不容易写坏,并且易于通过第三⽅⼯具快速查阅。当然相⽐较⾃定义的⼆进制结构⽂件,劣势是查询数据的⽅式会慢⼀点,你得构造出⼀个SQL语句,然后解析返回的数据才可以。不过这其实并不是问题,⼤量频繁的查询操作我们都定义在缓存⾥,全部交由Redis来解决了。除了查询历史数据,我们⼀般都不需要访问数据库。
数据库另外⼀个优势就是历史库。我们可以把很久远的数据导到历史库⾥,这⽐⾃⼰搞⼀个⼆进制⽂件,然后要⽀持分割历史到历史库⾥要⽅便,这种复杂的操作,我不是不相信⾃⼰写代码的能⼒,⽽是我觉得何必呢?
既然经验证明存⽂件都没有问题,那么数据库我选⽤SQLite应该不会出⼤问题,如果出问题了,我改⽤⾮嵌⼊式数据库就⾏了。等性能问题即将出现的时候,profile⼀下,问题若出在SQLite,再改不迟。
线程分配/管理
接下来说⼀说线程分配管理模块。这个模块的设想是怎么出来的呢?显然,并不是瞎想出来的。
我们先来回顾⼀下我们经常⽤到的⼏个⽹络库,⽐⽅说Libevent,ASIO。这些类库没有在业务逻辑⾥⾯⾃⼰搞线程池,说神马⾃⼰创建⼀个线程然后跑。最多就是⽀持⼀下线程安全。该有锁的地⽅配好⼀把锁。所以,这个基本特性也告诉我,我的⽹络层我的数据层这些,他们都不应该⾃⼰去分配线程。这些层级要做的就是要确保线程安全,这样才能⽅便做横向拓展。
要实现这个⽬的,就需要⽹络层对象、数据层对象有⼀个dependence injection的构造函数,传⼊⼀个libevent event base wrapper对象。⼀个对象只能跑在⼀个线程上。那么⼀个客户端session到时候就只在指定的⼀个(我希望是这样,避免不必要的线程切换)或两个(看⽹络
层和数据层这些是在同⼀条线程上跑还是分开跑)线程上跑。在这样的设计下,加之不同客户端之间其实业务⾏为都是独⽴的,包括数据都可以是独⽴的。那么不同线程间的客户session都是相互不影响的。有了这个分析结果,再配合thread_local,你在业务层⾯就不需要锁了。所以接下来通过增加线程的⽅式可以很轻松做到横向的多线程扩展。是不是?
未完,待续,to be continued。。。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论