撮合引擎开发:开篇数据库
撮合引擎开发:数据结构设计数据结构
业务流程
前面的几篇文章已经陆续讲到了黑箱内部的一些设计,包括核心的软件结构、数据结构、目录结构等。而从本小节开始,咱们将会更加深刻,来解密黑箱内部的更多设计和实现细节。post
解密黑箱的第一步就是要清楚其内部对数据的处理流程是怎样的。当咱们要设计一个新系统的时候,也是同样的,第一步要梳理清楚业务流程和数据流向。对撮合引擎来讲,就是要了解:从输入到输出,中间都通过了哪些处理流程。性能
前面的文章已经讲过,本撮合引擎定义了三种输入:开启撮合、处理订单、关闭撮合。后面就分别来看看这三种输入背后的流程。线程
开启撮合
开启撮合便是开启某个交易标的(交易对)的撮合引擎,未开启撮合的交易标的是没法处理订单的,而已经开启了撮合的交易标的也没法再次开启,否则就会出现同时有两个引擎处理同个交易标的的订单,这是不合理的,同个交易标的的订单只能由一个引擎串行来处理。设计
为何不能并行呢?若是同一交易标的的订单能够用多个引擎并行处理的话,那至少会产生几个问题:3d
- **成交价以哪一个为准?**理论上,每一时刻只能有一个成交价,那并行以后,就会产生多个成交价,那成交价就难以肯定了。
- **如何维护统一的委托帐本?**理论上,每一个交易标的有一本保存了全部委托单的委托帐本,那并行以后,如何在多个引擎之间维护这个统一的帐本呢?若是用数据库统一维护,那无疑会减低撮合性能;若是分为多个子帐本,那就很难保证价格优先、时间优先的原则。
以上这两个问题都很差解决,所以,只能先对全部订单进行定序,而后丢入引擎进行串行处理。
说到定序,天然就须要一个定序队列,所以开启撮合时须要初始化对应交易标的的订单定序队列。初始化好定序队列后,就能够真正启动对应交易标的的引擎了。在 Go 程序中,每一个交易标的的引擎是以独立 goroutine 运行的;而在其余语言,好比 Java,则是以独立线程来运行。
引擎启动以后,须要先初始化交易委托帐本,用来保存委托单。以后就等待定序队列有订单的时候逐个取出来处理了。
另外,再考虑一个场景,撮合程序重启时会发生什么?对于开启了撮合的交易标的,重启后是否须要恢复呢?须要的话,那如何恢复呢?最简单的方案固然是使用缓存,用 Redis 将开启了撮合的交易标的缓存起来,重启时从 Redis 加载并从新开启这些交易标的便可。
所以,触发开启撮合的场景其实有两个,一是接口的主动调用触发的,二是程序重启后从 Redis 缓存自动加载启动的。
最后,开启撮合的结果是同步返回的,所以,它没有异步的输出。
总结下,开启撮合的内部流程大体以下:
处理订单
开启撮合以后,就能够接收处理订单的输入了。撮合程序接收处处理订单的请求时,第一步须要作一些检查,包括每一个参数是否有效、订单是否重复或存在、对应交易标的的引擎是否已经开启等。经过了检查以后,就能够将整个订单缓存到 Redis,接着添加到对应交易标的的定序队列中去,等待对应交易标的的引擎消费它进行撮合处理。这个流程以下图:
当订单成功添加到定序队列中后,接口就能够同步返回成功的响应结果了。后续的处理结果则是经过异步的 MQ 进行输出了。交易标的的引擎接收到订单后,根据不一样状况会产生不一样的输出结果。
咱们知道,处理订单有两种 action:下单和撤单。撤单的业务逻辑很简单,就是从交易委托帐本中查询该订单是否存在,若存在则从委托帐本中删除该订单,而后输出撤单成功的撤单结果;若不存在则输出撤单失败的撤单结果。下单的业务逻辑则比较复杂,还要根据不一样的订单类型做不一样处理。写做此文时的撮合程序版本支持 6 种不一样的 type,包括两种限价类型和四种市价类型。下面就来分别讲解不一样订单类型的下单在不一样条件下会有怎样的结果。
- limit:普通限价。当委托帐本里存在能与该订单匹配成交的委托单时,则可能生成一条或多条成交记录,每条成交记录都将产生异步输出;当委托帐本里没有可匹配的委托单时,则将该订单(所有数量或剩余数量)添加到委托帐本中,这时不会产生任何输出。
- limit-ioc:IOC限价-即时成交剩余撤销。当委托帐本里存在能与该订单匹配成交的委托单时,则可能生成一条或多条成交记录,每条成交记录都将产生异步输出;当委托帐本里没有可匹配的委托单时,则将该订单(所有或剩余数量)进行撤单处理,这时会产生一条撤单成功的输出。
- market:默认市价-即时成交剩余撤销。和 IOC 限价同样,当委托帐本里与该订单相反方向的订单队列里(也称对手方)存在委托单时,则可能生成一条或多条成交记录,每条成交记录都将产生异步输出;当委托帐本里对手方没有委托单时,则将该订单(所有或剩余数量)进行撤单处理,这时会产生一条撤单成功的输出。与 IOC 限价不一样的在于:IOC 限价订单是由用户指定了委托价格的,而市价则无需指定委托价格,会直接与对手方的头部委托单成交,直到该订单已所有成交或对手方再无委托单为止。
- market-top5:市价-最优五档即时成交剩余撤销。market 能够与对手方全部价格档位的订单成交,但 market-top5 最多只会和对手方的五个价格档位内的订单成交,超出五档外的订单将不会成交。剩余未成交的都将作撤单处理并产生一条撤单成功的输出。
- market-top10:市价-最优十档即时成交剩余撤销。最多只会和对手方的十个价格档位内的订单成交。
- market-opponent:市价-对手方最优价。若是对手方没有订单,则直接对该订单进行撤单处理并产生一条撤单成功的输出;若是对手方有订单,那最多只会成交一档,若是还剩有未成交的量,那将以对手方一档的价格转为限价单并添加到委托帐本中,此时不会产生输出。
用图可表示以下:
另外,每一个处理订单的请求——不论是下单仍是撤单,也都会缓存到 Redis 里,产生变动时还会更新缓存。这样,程序重启后就能够恢复订单了。
关闭撮合
当某个交易标的准备下架、或取消交易、或暂停交易时,都须要关闭引擎。关闭引擎以前,上游服务最好先中止调用处理订单的接口,否则可能会出现一些非预期的错误,虽然程序已经作了容错处理。
关闭引擎时,一样也有些简单的判断,好比判断该交易标的的引擎是否已经开启,未开启的引擎天然没法关闭。
关闭引擎时,若是定序队列中还存在未处理的订单,那应该等这些订单处理完才真正关闭引擎。
最后,也要清除缓存,将该交易标的的全部订单都从缓存中清除。
关闭引擎的结果也是同步返回的,全部也没有异步的输出。
流程图也比较简答:
小结
本小节讲解了撮合黑箱内部的核心业务流程,包括开启撮合、处理订单、关闭撮合三个输入各自的内部逻辑。理解了这些流程以后,下一篇咱们开始来说代码实现。
惯例留几个思考题:若是关闭撮合的同时还有下单的并发请求,是否容易产生问题?若是有,哪里会产生?什么问题?能如何解决?