基於Java的數字貨幣交易系統的架構設計與開發
前言
無論是股票交易系統,還是數字貨幣交易系統,都離不開撮合交易引擎,這是交易平臺的心臟。同時,一個優秀的架構設計也會讓交易平臺的運維和持續開發更加容易。本文基於對開源專案的深入研究,總結了數字貨幣交易系統的架構設計。
本文參考了開源專案:https://gitee.com/cexchange/CoinExchange
關於撮合交易系統
撮合技術主要是從資料庫撮合技術向記憶體撮合技術發展,這是因為資料庫撮合技術越來越無法滿足金融交易對於高可靠性、高效能、強安全性、可擴充套件性以及易維護性的需求。金融(幣幣)交易撮合系統中包括以下幾個核心模組:
- 使用者:終端使用者委託報價與數量,生成訂單傳送至交易平臺。
- 閘道器:負責收集使用者訂單,並將其派發給撮合引擎。
- 撮合引擎:交易系統中的核心部分,用於接收訂單並根據業務邏輯實現訂單 撮合同時生成交易記錄,隨後給予使用者交易結果反饋。
- 資料庫:用來存放交易過程中的訂單和交易記錄,實現資料持久化。
- 訊息佇列:一般用於訂單訊息的傳輸
關於技術選型
一個交易所平臺的技術架構主要考慮安全性、分散式、易擴充套件、容錯性、低延時、高併發等特性,以及熔斷機制、服務註冊和發現、訊息服務、服務閘道器、安全認證、記憶體資料庫、關係型資料庫等各種選項,最終形成了如下技術選型:
- 分散式基礎進行架構SpringCloud與Dubbo之間二選一,由於SpringCloud更加知名,SpringCloud的程式設計師更好招聘,有利於系統的長期運維升級,而且SpringCloud是基於SpringBoot開發,比較有親切感,所以選擇了SpringCloud, 其實由於阿里系的強大影響,國內Dubbo使用更加廣泛,不同的團隊可以根據自己的情況選擇。
- 引入Hystrix斷路器作為容錯保護模組,防止單個服務的故障,耗盡整個撮合系統容器的執行緒資源,避免分散式環境裡大量級聯失敗。對通過第三方客戶端訪問依賴服務出現失敗、拒絕、超時或短路時執行回退邏輯。
- 採用Eureka作為服務註冊與發現中心,實現中間層服務,以達到負載均衡和中間層服務故障轉移的目的。
- 服務閘道器Spring Cloud Gateway 與 Zuul 的選型,選擇了Zuul,因為名字短一些。
- 引入SpringCloud Security安全認證模組用於構建安全的應用程式和服務,SpringCloud Security在Spring Boot和Spring Security OAuth2的基礎上,可以快速建立和實現常見的安全認證方式,如單點登入,令牌中繼和令牌交換等。
- 引入Redis作為記憶體資料庫,兼做系統資料快取和記憶體計算。
- 使用MySQL作為關係資料庫,效能測試非常過關,而且對熟悉MYSQL的程式設計師非常友好。
- 訊息佇列中介軟體MQ採用了Kafka, 具有超高效能體現。
關於交易所架構設計
基於SpringCloud開發基於微服務架構的交易平臺,首先需要對SpringCloud的基礎架構有所瞭解,我們熟知的SpringCloud微服務架構如下圖所示:
由於篇幅關係,本文就不對SpringCloud的技術架構進行詳細解讀了。
在SpringCloud這個優秀的微服務框架基礎之上,如何構建一個交易系統呢?開源專案CoinExchange對交易所的架構做了如下架構設計:
將撮合交易引擎、API等拆分作為單獨的服務,基於SpringCloud構建了一個精簡的交易所架構。
部署圖如下:
關於撮合交易引擎
採用記憶體撮合的方式進行,以Kafka做撮合訂單資訊傳輸,MongoDB持久化訂單成交明細,MySQL記錄訂單總體成交。其中行情模組主要負責訂單成交持久化、行情生成、行情推送等服務,包括:
- K線資料,間隔分別為:1分鐘、5分鐘、15分鐘、30分鐘、1小時、1天、1周、1月
- 所有交易對的市場深度(market depth)資料
- 所有交易對的最新價格
- 最近成交的交易對
記憶體撮合交易支援的模式
- 限價訂單與限價訂單撮合
- 市價訂單與限價訂單撮合
- 限價訂單與市價訂單撮合
- 市價訂單與市價訂單撮合
撮合邏輯過程如下圖所示:
示例程式碼如下:
1 /** 2 * 限價委託單與限價佇列匹配 3 * @param lpList 限價對手單佇列 4 * @param focusedOrder 交易訂單 5 */ 6 public void matchLimitPriceWithLPList(TreeMap<BigDecimal,MergeOrder> lpList, ExchangeOrder focusedOrder,boolean canEnterList){ 7 List<ExchangeTrade> exchangeTrades = new ArrayList<>(); 8 List<ExchangeOrder> completedOrders = new ArrayList<>(); 9 synchronized (lpList) { 10 Iterator<Map.Entry<BigDecimal,MergeOrder>> mergeOrderIterator = lpList.entrySet().iterator(); 11 boolean exitLoop = false; 12 while (!exitLoop && mergeOrderIterator.hasNext()) { 13 Map.Entry<BigDecimal,MergeOrder> entry = mergeOrderIterator.next(); 14 MergeOrder mergeOrder = entry.getValue(); 15 Iterator<ExchangeOrder> orderIterator = mergeOrder.iterator(); 16 //買入單需要匹配的價格不大於委託價,否則退出 17 if (focusedOrder.getDirection() == ExchangeOrderDirection.BUY && mergeOrder.getPrice().compareTo(focusedOrder.getPrice()) > 0) { 18 break; 19 } 20 //賣出單需要匹配的價格不小於委託價,否則退出 21 if (focusedOrder.getDirection() == ExchangeOrderDirection.SELL && mergeOrder.getPrice().compareTo(focusedOrder.getPrice()) < 0) { 22 break; 23 } 24 while (orderIterator.hasNext()) { 25 ExchangeOrder matchOrder = orderIterator.next(); 26 //處理匹配 27 ExchangeTrade trade = processMatch(focusedOrder, matchOrder); 28 exchangeTrades.add(trade); 29 //判斷匹配單是否完成 30 if (matchOrder.isCompleted()) { 31 //當前匹配的訂單完成交易,刪除該訂單 32 orderIterator.remove(); 33 completedOrders.add(matchOrder); 34 } 35 //判斷交易單是否完成 36 if (focusedOrder.isCompleted()) { 37 //交易完成 38 completedOrders.add(focusedOrder); 39 //退出迴圈 40 exitLoop = true; 41 break; 42 } 43 } 44 if(mergeOrder.size() == 0){ 45 mergeOrderIterator.remove(); 46 } 47 } 48 } 49 //如果還沒有交易完,訂單壓入列表中 50 if (focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0 && canEnterList) { 51 addLimitPriceOrder(focusedOrder); 52 } 53 //每個訂單的匹配批量推送 54 handleExchangeTrade(exchangeTrades); 55 if(completedOrders.size() > 0){ 56 orderCompleted(completedOrders); 57 TradePlate plate = focusedOrder.getDirection() == ExchangeOrderDirection.BUY ? sellTradePlate : buyTradePlate; 58 sendTradePlateMessage(plate); 59 } 60 } 61 62 /** 63 * 限價委託單與市價佇列匹配 64 * @param mpList 市價對手單佇列 65 * @param focusedOrder 交易訂單 66 */ 67 public void matchLimitPriceWithMPList(LinkedList<ExchangeOrder> mpList,ExchangeOrder focusedOrder){ 68 List<ExchangeTrade> exchangeTrades = new ArrayList<>(); 69 List<ExchangeOrder> completedOrders = new ArrayList<>(); 70 synchronized (mpList) { 71 Iterator<ExchangeOrder> iterator = mpList.iterator(); 72 while (iterator.hasNext()) { 73 ExchangeOrder matchOrder = iterator.next(); 74 ExchangeTrade trade = processMatch(focusedOrder, matchOrder); 75 logger.info(">>>>>"+trade); 76 if(trade != null){ 77 exchangeTrades.add(trade); 78 } 79 //判斷匹配單是否完成,市價單amount為成交量 80 if(matchOrder.isCompleted()){ 81 iterator.remove(); 82 completedOrders.add(matchOrder); 83 } 84 //判斷吃單是否完成,判斷成交量是否完成 85 if (focusedOrder.isCompleted()) { 86 //交易完成 87 completedOrders.add(focusedOrder); 88 //退出迴圈 89 break; 90 } 91 } 92 } 93 //如果還沒有交易完,訂單壓入列表中 94 if (focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0) { 95 addLimitPriceOrder(focusedOrder); 96 } 97 //每個訂單的匹配批量推送 98 handleExchangeTrade(exchangeTrades); 99 orderCompleted(completedOrders); 100 } 101 102 103 /** 104 * 市價委託單與限價對手單列表交易 105 * @param lpList 限價對手單列表 106 * @param focusedOrder 待交易訂單 107 */ 108 public void matchMarketPriceWithLPList(TreeMap<BigDecimal,MergeOrder> lpList, ExchangeOrder focusedOrder){ 109 List<ExchangeTrade> exchangeTrades = new ArrayList<>(); 110 List<ExchangeOrder> completedOrders = new ArrayList<>(); 111 synchronized (lpList) { 112 Iterator<Map.Entry<BigDecimal,MergeOrder>> mergeOrderIterator = lpList.entrySet().iterator(); 113 boolean exitLoop = false; 114 while (!exitLoop && mergeOrderIterator.hasNext()) { 115 Map.Entry<BigDecimal,MergeOrder> entry = mergeOrderIterator.next(); 116 MergeOrder mergeOrder = entry.getValue(); 117 Iterator<ExchangeOrder> orderIterator = mergeOrder.iterator(); 118 while (orderIterator.hasNext()) { 119 ExchangeOrder matchOrder = orderIterator.next(); 120 //處理匹配 121 ExchangeTrade trade = processMatch(focusedOrder, matchOrder); 122 if (trade != null) { 123 exchangeTrades.add(trade); 124 } 125 //判斷匹配單是否完成 126 if (matchOrder.isCompleted()) { 127 //當前匹配的訂單完成交易,刪除該訂單 128 orderIterator.remove(); 129 completedOrders.add(matchOrder); 130 } 131 //判斷焦點訂單是否完成 132 if (focusedOrder.isCompleted()) { 133 completedOrders.add(focusedOrder); 134 //退出迴圈 135 exitLoop = true; 136 break; 137 } 138 } 139 if(mergeOrder.size() == 0){ 140 mergeOrderIterator.remove(); 141 } 142 } 143 } 144 //如果還沒有交易完,訂單壓入列表中,市價買單按成交量算 145 if (focusedOrder.getDirection() == ExchangeOrderDirection.SELL&&focusedOrder.getTradedAmount().compareTo(focusedOrder.getAmount()) < 0 146 || focusedOrder.getDirection() == ExchangeOrderDirection.BUY&& focusedOrder.getTurnover().compareTo(focusedOrder.getAmount()) < 0) { 147 addMarketPriceOrder(focusedOrder); 148 } 149 //每個訂單的匹配批量推送 150 handleExchangeTrade(exchangeTrades); 151 if(completedOrders.size() > 0){ 152 orderCompleted(completedOrders); 153 TradePlate plate = focusedOrder.getDirection() == ExchangeOrderDirection.BUY ? sellTradePlate : buyTradePlate; 154 sendTradePlateMessage(plate); 155 } 156 }
關於區塊鏈錢包對接
每個幣種對應不同的資料訪問方式,大部分割槽塊鏈專案的錢包操作方式是相同的或十分相似的,比如BTC、LTC、BCH、BSV、BCD等比特幣衍生幣,其API操作方式幾乎一樣;再比如ETH,當你掌握一個合約幣種的操作,其他基於ETH發行的數字貨幣的操作方式幾乎一樣。所以,基本上當你花時間弄懂了一個,就懂了一堆幣種。
本專案使用的錢包操作方案也是不同的,也儘可能的為大家展示了不同用法:
- 如BTC、USDT,使用的自建全節點,現在差不多需要300G硬碟空間;
- 如ETH,使用的是自建輕節點(參考文章),因為全節點需要硬碟空間太大;
- 如BCH、BSV等,使用的是第三方區塊鏈瀏覽器獲取資料;
- 如XRP,官方就已經提供了訪問區塊資料的介面(Ripple API GitHub地址)
一般而言,當交易所來往資金量不大的時候,你可以自己摸索,但是當交易所資金量大了以後,如果你對自己操作錢包不太放心,你也可以使用第三方的錢包服務,當然,這需要你與錢包服務商進行談判,付個年費什麼的。
下圖是關於交易平臺充值邏輯的一個簡單時序圖:
總結
通過以上的說明及圖示,我們基本上對交易所的整體架構有了一定的認知。
感謝
最後感謝開源交易所專案給與我學習的機會!
Java開源交易平臺專案:https://gitee.com/cexchange/CoinExchange