Leetcode 有些题目还是挺有趣的,这周的Leetcode contest,第二道题为 Number of Orders in the Backlog,需要实现一个撮合订单的数据结构。
先来看一下题目,Leetcode 1801. Number of Orders in the Backlog。
简单来说,订单分为买单和卖单两种类型,用户下了一个买单后,需要在卖单集合里找到价格等于或小于这个买单的订单,然后成交。
相反,用户下了一个卖单后,需要在买单集合里找到价格等于或大于这个卖单的订单,然后成交。
撮合引擎复杂在于,订单是存在数据库里的,从数据库里找到匹配的订单也是可以的,但一个订单就对应一个数据库查询,效率恐怕不高。
一个解决思路是,把这些数据放到内存里。国内的很多数字货币交易所的撮合引擎,就是这么一个发展历程,从数据库撮合迭代为内存撮合。
把数据放在内存,听起来有点奇怪,但要是考虑一下亿级别的订单,放到一个Map里所需占用的内存,按现在机器的配置,完全是小case。更何况,内存里还不会有亿级别的订单,正在挂着的订单远远达不到这个数。
写《重构》的作者 Martin Fowler 十年前写过一篇很出名的文章介绍 LMAX 公司(LMAX是伦敦的一家外汇交易所)开源的 Disruptor,以及LMAX 架构,它的一个核心概念就是把数据放内存里,由此实现了每秒处理600万订单的超高效率。
国内的头部交易所,据我所知也是用的内存撮合。币安的技术介绍文章,《币安交易平台技术栈首揭秘:什么样的架构体系能挡住全世界的黑客攻击》,里面就提到从数据库撮合到内存撮合的一个演化。
LMAX架构巧妙在于,它并不认为多核CPU和多线程能解决问题,因为锁是一种很复杂的东西,而且并不见得效率高,它把锁去掉,降低整个系统的复杂度,并且用无锁队列 RingBuffer 来处理生产者消费者问题。
当然也并非所有的交易所都用LMAX架构。据悉,火币的撮合引擎用的 Clojure,这就是另一种并发模型了。
回到正题,撮合引擎很复杂,但它的核心数据结构是很简单的。
首先,买单需要找到卖单集合里价格最低的订单,卖单需要找到买单集合里价格最高的订单,按这个规则,我们需要一个有序的集合。
另外,撮合的规则一般是“价格优先,时间优先”,所以我们需要它可以根据价格和时间来排序。
这么一看,比较符合的数据结构是TreeMap,插入、查找的时间复杂度都是log(n)。
首先我们需要定义两个TreeMap,一个是买单集合,一个卖单集合。这两个TreeMap的key为价格,买单按价格从高到低排序,卖单从低到高排序。
因为撮合规则是“价格优先,时间优先”,我们还需要按时间优先排序,TreeMap里再套一个TreeMap(由于题目不涉及时间这个概念,这里简化用LinkedList表示)
可以定义一个DepthLine类,用于表示相同价格的一组订单。
val buy = TreeMap<Int, DepthLine> { a, b ->
b - a
}
val sell = TreeMap<Int, DepthLine> { a, b ->
a - b
}
定义订单数据结构和DepthLine,里面的数据结构应为TreeMap,这样就能把价格相同的订单按时间排序,但由于题目没有时间一说,我就偷懒不写了。
其实这道题可以设计得更好,加上时间的概念,订单按顺序传入,这样写出来的代码和工业环境能用的就差不了多少了。批量加减反而让这道题成了一道算法技巧题,毫无优雅可言。
class DepthLine(val price: Int) {
val orderList = LinkedList<Order>()
fun size(): Int {
return orderList.size
}
fun offer(order: Order) {
orderList.offer(order)
}
fun poll() {
orderList.poll()
}
}
class Order(val price: Int, val type: Int)
剩下的match逻辑就很简单了,用户下了一个买单后,需要在卖单集合里找到价格等于或小于这个买单的订单,然后成交。相反,用户下了一个卖单后,需要在买单集合里找到价格等于或大于这个卖单的订单,然后成交。如果找不到对手单,把订单放到对应的订单集合里。
private fun match(order: Order, book: TreeMap<Int, DepthLine>, oppositeBook: TreeMap<Int, DepthLine>) {
var matched = false
for ((price, depthLine) in oppositeBook) {
if (order.type == SELL) {
if (price >= order.price && depthLine.size() > 0) {
depthLine.poll()
if (depthLine.size() == 0) {
oppositeBook.remove(price)
}
matched = true
}
} else {
if (price <= order.price && depthLine.size() > 0) {
depthLine.poll()
if (depthLine.size() == 0) {
oppositeBook.remove(price)
}
matched = true
}
}
break
}
if (!matched) {
var depthLine = book[order.price]
if (depthLine == null) {
depthLine = DepthLine(order.price)
book[order.price] = depthLine
}
depthLine.offer(Order(order.price, order.type))
}
}
最后贴一下完整代码吧。不过这个代码运行是超时,题目希望做个批量的加减,而我是逐个订单处理了,数据一大自然就超时了。真实的撮合引擎中,订单都是按顺序处理的,还有个叫“定序”的过程。这里改为不超时也很简单,但改了恐怕代码就不能优美地展示这个撮合模型了,暂时就不处理了。
class `Number of Orders in the Backlog` {
val buy = TreeMap<Int, DepthLine> { a, b ->
b - a
}
val sell = TreeMap<Int, DepthLine> { a, b ->
a - b
}
val BUY = 0
val SELL = 1
val mod = 1000000007
fun getNumberOfBacklogOrders(orders: Array<IntArray>): Int {
for (order in orders) {
for (i in 0 until order[1]) {
match(Order(order[0], order[2]))
}
}
var res = 0
buy.forEach {
res += it.value.size()
}
sell.forEach {
res += it.value.size()
}
return res % mod
}
private fun match(order: Order) {
if (order.type == SELL) {
match(order, sell, buy)
} else {
match(order, buy, sell)
}
}
private fun match(order: Order, book: TreeMap<Int, DepthLine>, oppositeBook: TreeMap<Int, DepthLine>) {
var matched = false
for ((price, depthLine) in oppositeBook) {
if (order.type == SELL) {
if (price >= order.price && depthLine.size() > 0) {
depthLine.poll()
if (depthLine.size() == 0) {
oppositeBook.remove(price)
}
matched = true
}
} else {
if (price <= order.price && depthLine.size() > 0) {
depthLine.poll()
if (depthLine.size() == 0) {
oppositeBook.remove(price)
}
matched = true
}
}
break
}
if (!matched) {
var depthLine = book[order.price]
if (depthLine == null) {
depthLine = DepthLine(order.price)
book[order.price] = depthLine
}
depthLine.offer(Order(order.price, order.type))
}
}
class DepthLine(val price: Int) {
val orderList = LinkedList<Order>()
fun size(): Int {
return orderList.size
}
fun offer(order: Order) {
orderList.offer(order)
}
fun poll() {
orderList.poll()
}
}
class Order(val price: Int, val type: Int)
}