撮合引擎開發:開篇數據庫
撮合引擎開發:數據結構設計數據結構
業務流程
前面的幾篇文章已經陸續講到了黑箱內部的一些設計,包括核心的軟件結構、數據結構、目錄結構等。而從本小節開始,咱們將會更加深刻,來解密黑箱內部的更多設計和實現細節。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 裏,產生變動時還會更新緩存。這樣,程序重啓後就能夠恢復訂單了。
關閉撮合
當某個交易標的準備下架、或取消交易、或暫停交易時,都須要關閉引擎。關閉引擎以前,上游服務最好先中止調用處理訂單的接口,否則可能會出現一些非預期的錯誤,雖然程序已經作了容錯處理。
關閉引擎時,一樣也有些簡單的判斷,好比判斷該交易標的的引擎是否已經開啓,未開啓的引擎天然沒法關閉。
關閉引擎時,若是定序隊列中還存在未處理的訂單,那應該等這些訂單處理完才真正關閉引擎。
最後,也要清除緩存,將該交易標的的全部訂單都從緩存中清除。
關閉引擎的結果也是同步返回的,全部也沒有異步的輸出。
流程圖也比較簡答:
小結
本小節講解了撮合黑箱內部的核心業務流程,包括開啓撮合、處理訂單、關閉撮合三個輸入各自的內部邏輯。理解了這些流程以後,下一篇咱們開始來說代碼實現。
慣例留幾個思考題:若是關閉撮合的同時還有下單的併發請求,是否容易產生問題?若是有,哪裏會產生?什麼問題?能如何解決?