内存数据库市场上有很多成熟产品,这里不一一列举,主要针对一些有特色的系统做简单分析。在一个场景中,要用到内存数据库,最核心的需求是低延迟,至于高吞吐,事务,分布式,支持SQL,复杂查询,持久化等都是其次。
内存数据库场景选型时,往往是特化的。比如redis是针对传统数据库的热数据进行优化。SQLite虽然支持SQL,但主要应用于进程内部小型存储的简单应用,性能往往并不需要那么突出。FastDB是嵌入式内存数据库,和C/C++深度绑定,类似的还有BerkeleyDB。SAP的HANA和ORACLE的TimesTen也是很优秀的商业版。
我们以股票行情在内存中存储为例,讨论内存数据库的设计,非官方资料,仅仅猜测。期货中有"腿"的概念,债券有可议价的选项,这些是业务上复杂度,和技术没有直接关系。有读者问过我关于KDB+和DolphinDB,我没有用过,不做评价。
一、内存布局
1、每只股票行情属性是格式化,列的数量也是固定的,因此,每只股票行情可以类似于C/C++结构体方式存储于内存中。
2、每个交易所可交易的股票数量当天是固定的。于是,每个交易所的全部股票行情存储于内存,布局类似于一个二维数组,构成一个数据表。
3、股票的代码是唯一的,可以用哈希函数快速定位该股票行情在数组的下标。特别是上证和深证的股票代码是数字格式,更容易转换成下标。以此构建数据表的关键字索引。
4、每只股票行情相当于数据表的行,在内存中是连续存储的。每个字段相对于行首的偏移量是固定的,于是,就可以通过数据表的表头信息得到每只股票的每个字段的名字,类型,偏移量,从而计算出值。
二、读写更新
1、股票行情的数据表,读的频率通常认为高于写。可以通过股票代码哈希函数计算出对应下标,时间复杂度为O(1)。同时,因为在内存上是连续分布,所以对于CPU访问是友好的,避免缺页引起的性能损失。
2、国内的股票行情,包括期货行情,都是定时刷新,不是连续发布。在接收到行情时,可以采用双副本模式实现读写分离。读操作在主副本上执行,写操作在从副本上执行,在写操作全部完成后,进行主从副本切换。
3、行情更新更多的是指衍生数值的计算,可以在向从副本写入之前,在外部准备好,然后和基础数据合并在一块连续内存,一次性写入到从副本。
三、查找排序
1、定值排序,每只股票的属性,比如名称代码是固定值,顺序上是一致的,可以做预先排序。正序倒序都是固定,不需要太多讨论。
2、变值排序,这种排序的难度比较大。简单一点的就是搞一个红黑色树,用现成的C++的std::map。稍微麻烦点,搞一个跳表,虽然不是标准库,但实现难度远低于红黑树。之所以说难度比较大,是加入截取某个下表范围的股票用于终端显示,这种场景下,要快速实现,难度不低。比如,按照价格排序,终端显示序号在第101-150之间的股票。每次行情刷新,这个范围之内的股票基本都要发生变化的。
对于这种场景的需求,可以参考倒排序算法,每个节点包含三个元素{值,代码,该值包含的股票数量},我在博客中《提高链表随机访问效率的一种方案》提到的一个算法可以参考。而对于变化很小的价格这样的变值排序,还可以进一步优化。
3、查找基于排序,不再赘述。
四、其他问题
1、是否支持SQL语法。对于追求低延迟系统来说,往往是限制在特定场景中,通常不会追求支持SQL语法。
2、是否支持持久化。内存数据库的持久化,通常不会在本地完成,而是由旁路来完成。
3、是否支持高吞吐量。内存数据库核心是低延迟,主要功能是读取和计算,高吞吐量不是核心特性。虽然低延迟系统的吞吐量都不会太低,但高吞吐量还受限于网络传输,而且两个指标在极端情况下,是有冲突的,需要取舍。
4、是否支持快速恢复。如果内存数据库基于共享内存,那么即使进程崩溃,只要服务器没有重启,共享内存中的数据依然留存,天然支持快速恢复。除此之外,内存数据库的快速恢复只需要通过内存块拷贝,就能自动实现。
5、是否支持重演。虽然行情是时序数据,内存数据库不是时序,而一种快照。数据重演,实现幂等性,由消息队列来实现。