撮合引擎開發(fā):開篇
撮合引擎開發(fā):MVP版本
撮合引擎開發(fā):數(shù)據(jù)結(jié)構(gòu)設(shè)計
撮合引擎開發(fā):對接黑箱
撮合引擎開發(fā):解密黑箱流程
業(yè)務(wù)流程
前面的幾篇文章已經(jīng)陸續(xù)講到了黑箱內(nèi)部的一些設(shè)計骤公,包括核心的軟件結(jié)構(gòu)胁塞、數(shù)據(jù)結(jié)構(gòu)跋选、目錄結(jié)構(gòu)等。而從本小節(jié)開始夺溢,我們將會更加深入,來解密黑箱內(nèi)部的更多設(shè)計和實現(xiàn)細節(jié)替久。
解密黑箱的第一步就是要清楚其內(nèi)部對數(shù)據(jù)的處理流程是怎樣的研铆。當我們要設(shè)計一個新系統(tǒng)的時候,也是一樣的劳较,第一步要梳理清楚業(yè)務(wù)流程和數(shù)據(jù)流向驹止。對撮合引擎來說浩聋,就是要了解:從輸入到輸出,中間都經(jīng)過了哪些處理流程臊恋。
前面的文章已經(jīng)講過衣洁,本撮合引擎定義了三種輸入:開啟撮合、處理訂單抖仅、關(guān)閉撮合坊夫。后面就分別來看看這三種輸入背后的流程。
開啟撮合
開啟撮合即是開啟某個交易標的(交易對)的撮合引擎撤卢,未開啟撮合的交易標的是無法處理訂單的环凿,而已經(jīng)開啟了撮合的交易標的也無法再次開啟,不然就會出現(xiàn)同時有兩個引擎處理同個交易標的的訂單放吩,這是不合理的智听,同個交易標的的訂單只能由一個引擎串行來處理。
為什么不能并行呢渡紫?如果同一交易標的的訂單可以用多個引擎并行處理的話到推,那至少會產(chǎn)生幾個問題:
- 成交價以哪個為準?理論上腻惠,每一時刻只能有一個成交價环肘,那并行之后欲虚,就會產(chǎn)生多個成交價集灌,那成交價就難以確定了。
- 如何維護統(tǒng)一的委托賬本复哆?理論上欣喧,每個交易標的有一本保存了所有委托單的委托賬本,那并行之后梯找,如何在多個引擎之間維護這個統(tǒng)一的賬本呢唆阿?如果用數(shù)據(jù)庫統(tǒng)一維護,那無疑會減低撮合性能锈锤;如果分為多個子賬本驯鳖,那就很難保證價格優(yōu)先、時間優(yōu)先的原則久免。
以上這兩個問題都不好解決浅辙,因此,只能先對所有訂單進行定序阎姥,然后丟入引擎進行串行處理记舆。
說到定序,自然就需要一個定序隊列呼巴,因此開啟撮合時需要初始化對應(yīng)交易標的的訂單定序隊列泽腮。初始化好定序隊列后御蒲,就可以真正啟動對應(yīng)交易標的的引擎了。在 Go 程序中诊赊,每個交易標的的引擎是以獨立 goroutine 運行的厚满;而在其他語言,比如 Java碧磅,則是以獨立線程來運行痰滋。
引擎啟動之后,需要先初始化交易委托賬本续崖,用來保存委托單敲街。之后就等待定序隊列有訂單的時候逐個取出來處理了。
另外严望,再考慮一個場景多艇,撮合程序重啟時會發(fā)生什么?對于開啟了撮合的交易標的像吻,重啟后是否需要恢復(fù)呢峻黍?需要的話,那如何恢復(fù)呢拨匆?最簡單的方案當然是使用緩存姆涩,用 Redis 將開啟了撮合的交易標的緩存起來,重啟時從 Redis 加載并重新開啟這些交易標的即可惭每。
因此骨饿,觸發(fā)開啟撮合的場景其實有兩個,一是接口的主動調(diào)用觸發(fā)的台腥,二是程序重啟后從 Redis 緩存自動加載啟動的宏赘。
最后,開啟撮合的結(jié)果是同步返回的黎侈,因此察署,它沒有異步的輸出。
總結(jié)下峻汉,開啟撮合的內(nèi)部流程大致如下:
處理訂單
開啟撮合之后贴汪,就可以接收處理訂單的輸入了。撮合程序接收到處理訂單的請求時休吠,第一步需要做一些檢查扳埂,包括每個參數(shù)是否有效、訂單是否重復(fù)或存在蛛碌、對應(yīng)交易標的的引擎是否已經(jīng)開啟等聂喇。通過了檢查之后,就可以將整個訂單緩存到 Redis,接著添加到對應(yīng)交易標的的定序隊列中去希太,等待對應(yīng)交易標的的引擎消費它進行撮合處理克饶。這個流程如下圖:
當訂單成功添加到定序隊列中后,接口就可以同步返回成功的響應(yīng)結(jié)果了誊辉。后續(xù)的處理結(jié)果則是通過異步的 MQ 進行輸出了矾湃。交易標的的引擎接收到訂單后,根據(jù)不同情況會產(chǎn)生不同的輸出結(jié)果堕澄。
我們知道邀跃,處理訂單有兩種 action:下單和撤單。撤單的業(yè)務(wù)邏輯很簡單蛙紫,就是從交易委托賬本中查詢該訂單是否存在拍屑,若存在則從委托賬本中刪除該訂單,然后輸出撤單成功的撤單結(jié)果坑傅;若不存在則輸出撤單失敗的撤單結(jié)果僵驰。下單的業(yè)務(wù)邏輯則比較復(fù)雜,還要根據(jù)不同的訂單類型作不同處理唁毒。寫作此文時的撮合程序版本支持 6 種不同的 type蒜茴,包括兩種限價類型和四種市價類型。下面就來分別講解不同訂單類型的下單在不同條件下會有怎樣的結(jié)果浆西。
- limit:普通限價粉私。當委托賬本里存在能與該訂單匹配成交的委托單時,則可能生成一條或多條成交記錄近零,每條成交記錄都將產(chǎn)生異步輸出诺核;當委托賬本里沒有可匹配的委托單時,則將該訂單(全部數(shù)量或剩余數(shù)量)添加到委托賬本中秒赤,這時不會產(chǎn)生任何輸出猪瞬。
- limit-ioc:IOC限價-即時成交剩余撤銷憎瘸。當委托賬本里存在能與該訂單匹配成交的委托單時入篮,則可能生成一條或多條成交記錄,每條成交記錄都將產(chǎn)生異步輸出幌甘;當委托賬本里沒有可匹配的委托單時潮售,則將該訂單(全部或剩余數(shù)量)進行撤單處理,這時會產(chǎn)生一條撤單成功的輸出锅风。
- market:默認市價-即時成交剩余撤銷酥诽。和 IOC 限價一樣,當委托賬本里與該訂單相反方向的訂單隊列里(也稱對手方)存在委托單時皱埠,則可能生成一條或多條成交記錄肮帐,每條成交記錄都將產(chǎn)生異步輸出;當委托賬本里對手方?jīng)]有委托單時,則將該訂單(全部或剩余數(shù)量)進行撤單處理训枢,這時會產(chǎn)生一條撤單成功的輸出托修。與 IOC 限價不同的在于:IOC 限價訂單是由用戶指定了委托價格的,而市價則無需指定委托價格恒界,會直接與對手方的頭部委托單成交睦刃,直到該訂單已全部成交或?qū)κ址皆贌o委托單為止。
- market-top5:市價-最優(yōu)五檔即時成交剩余撤銷十酣。market 可以與對手方所有價格檔位的訂單成交涩拙,但 market-top5 最多只會和對手方的五個價格檔位內(nèi)的訂單成交,超出五檔外的訂單將不會成交耸采。剩余未成交的都將做撤單處理并產(chǎn)生一條撤單成功的輸出兴泥。
- market-top10:市價-最優(yōu)十檔即時成交剩余撤銷。最多只會和對手方的十個價格檔位內(nèi)的訂單成交虾宇。
- market-opponent:市價-對手方最優(yōu)價郁轻。如果對手方?jīng)]有訂單,則直接對該訂單進行撤單處理并產(chǎn)生一條撤單成功的輸出文留;如果對手方有訂單好唯,那最多只會成交一檔,如果還剩有未成交的量燥翅,那將以對手方一檔的價格轉(zhuǎn)為限價單并添加到委托賬本中骑篙,此時不會產(chǎn)生輸出。
用圖可表示如下:
另外森书,每個處理訂單的請求——不管是下單還是撤單靶端,也都會緩存到 Redis 里,產(chǎn)生變更時還會更新緩存凛膏。這樣杨名,程序重啟后就可以恢復(fù)訂單了。
關(guān)閉撮合
當某個交易標的準備下架猖毫、或取消交易台谍、或暫停交易時,都需要關(guān)閉引擎吁断。關(guān)閉引擎之前趁蕊,上游服務(wù)最好先停止調(diào)用處理訂單的接口,不然可能會出現(xiàn)一些非預(yù)期的錯誤仔役,雖然程序已經(jīng)做了容錯處理掷伙。
關(guān)閉引擎時,同樣也有些簡單的判斷又兵,比如判斷該交易標的的引擎是否已經(jīng)開啟任柜,未開啟的引擎自然無法關(guān)閉。
關(guān)閉引擎時,如果定序隊列中還存在未處理的訂單宙地,那應(yīng)該等這些訂單處理完才真正關(guān)閉引擎升熊。
最后,也要清除緩存绸栅,將該交易標的的所有訂單都從緩存中清除级野。
關(guān)閉引擎的結(jié)果也是同步返回的,所有也沒有異步的輸出粹胯。
流程圖也比較簡答:
小結(jié)
本小節(jié)講解了撮合黑箱內(nèi)部的核心業(yè)務(wù)流程蓖柔,包括開啟撮合、處理訂單风纠、關(guān)閉撮合三個輸入各自的內(nèi)部邏輯况鸣。理解了這些流程之后,下一篇我們開始來講代碼實現(xiàn)竹观。
慣例留幾個思考題:如果關(guān)閉撮合的同時還有下單的并發(fā)請求镐捧,是否容易產(chǎn)生問題?如果有臭增,哪里會產(chǎn)生懂酱?什么問題?能如何解決誊抛?