Skip to content

Spot交易核心模块:下单、定序、清算、归档

总体流程图

Core

收集所有API端的请求,并将其转发到Order模块进行处理。

Order

前置收单模块,负责处理用户的下单请求,包括验证配额和余额是否足够,锁定配额和余额,整理用户请求参数,并将请求通过MQ转发到定序器。

前置校验

Order校验一系列条件:

  • 用户配额是否足够(Quota)
  • 用户余额是否足够(Balance)
  • 提交的订单参数是否正确且符合交易限制(如价格、数量等)
  • 用户是否被禁止交易(如黑名单等)
  • 用户是否已通过KYC(Know Your Customer)认证
  • 用户是否超过最大挂单量限制
  • 用户提交的客户端订单号是否重复
  • 系统是否无法处理更多订单(如系统负载过高)
  • 该币种是否正在交易

Order模块将会规整用户的下单类型,具体包括:

NoTypeOrderSideQuantityModePriceBTC AmountUSDT AmountValuePriceAmountBackend BehaviorLockBuyOrSellShareOrVolumeMarketOrLimitLimitPriceSharesMarketVolume
1LIMITBUYAMOUNT66616661666 USDTBUYShareLIMIT6661
2LIMITBUYVALUE6661000066610000/666to AMOUNT10000/666*666BUYShareLIMIT10000/666
3MARKETBUYAMOUNT11 * Ask1 * 1.051to VALUE1 * Ask1 * 1.05BUYVolumeMARKET11 * Ask1 * 1.05
4MARKETBUYVALUE100001000010000 USDTBUYVolumeMARKET10000
5LIMITSELLAMOUNT666166611 BTCSELLShareLIMIT6661
6LIMITSELLVALUE6661000066610000/666to AMOUNT10000/666 BTCSELLShareLIMIT66610000/666
7MARKETSELLAMOUNT111 BTCSELLShareMARKET1
8MARKETSELLVALUE1000010000/Bid1/1to AMOUNT10000/Bid1/1 BTCSELLShareMARKET10000/Bid1/1.05

TCC

Order 在检查用户配额和余额时,将会进行TCC远程调用(TccCheckDistributedResources),锁定配额和余额。如果任何一个资源锁定失败或Try超时,TCC业务将会回滚,释放配额和余额。

在TCC Try之前,Order会首先进行远程只读检查,降低TCC失败概率。

开始TCC时,Order会将该TCC请求存储到数据库中

  • TCC状态=TccGlobalStatusTrying
  • TCC类型=TransactionTypeOrderLock
  • GlobalId=OrderId
  • 资源分支
    • Quota
    • BalanceId

调用TCC Try,检查返回值

  • err != nil:失败,返回未知内部错误代码到用户侧
  • TccCode_Success: 成功,继续执行
  • TccCode_Failed: 失败,有具体业务错误代码,返回业务错误代码到用户侧
  • TccCode_Timeout: 超时,返回未知内部错误代码到用户侧

由于调用Try前会做远程只读检查,TCC Try失败的概率非常低。如果失败,将会有定时任务清理TCC状态为TccGlobalStatusTrying的过期TCC记录。

成功后,数据库将会记录三项内容:

  • UserRequest:用户原始请求
  • Order/OrderHistory:用户订单(历史订单)
  • TCC状态更新:
    • TCC状态=TccGlobalStatusConfirming

如数据库写入成功,将会继续执行TCC Confirm。设置TCC状态为TccGlobalStatusConfirming。成功后设置为TccGlobalStatusConfirmed。

如果数据库写入失败且之前触发过TCC,将会设置TCC状态为TccGlobalStatusCancelling,成功后设置为TccGlobalStatusCancelled。

发送订单

以上所有操作完成后,Order将会将用户请求通过消息队列发送到定序器(Ingress)进行处理。同时用户收到返回的订单ID。

丢失订单处理

发送到MQ这个动作可能会失败,导致订单未能被定序器接收。为了确保订单不丢失,Ingress模块会每隔15秒查询6分钟前的订单,并重新发送到撮合引擎。

撮合引擎对于过期订单的容忍度为3分钟(Ingress设置),因此发送6分钟前的订单可以确保订单在撮合引擎的容忍度内。

超过3分钟的订单将会被撮合引擎输出taker_cancel的撮合动作,表示订单已过期。

Ingress

定序器,消费来自Order模块的下单请求MQ,形成全局唯一的订单序列号(Sequence,连续不断单调递增),并将请求批量发送至交易引擎进行处理。

Ingress每个币对只可以启动一个实例,以确保订单的顺序性和一致性。

Ingress同时负责维护撮合引擎的状态,包括订单簿、最新的Sequence号、撮合参数等。Ingress定期检查撮合引擎的状态,如状态失效,将会引爆自身,并重新启动。

恢复撮合引擎状态

Ingress每一次重新启动时,将会在等待以下条件后,从数据库中加载最新的撮合引擎状态,并据此恢复撮合引擎订单簿(ResetAndWarmUpEngine)。

  • 等待Engine运行并空置
  • 等待Clearing空置(没有未处理的OrderResults)
  • 且连续3次15秒(共45秒)满足以上条件

恢复订单簿时,Ingress将会从数据库中加载:

  • status=OPEN、type=LIMIT的订单(已在订单簿上)
  • status=NEW的订单(未在订单簿上)

这些订单发送到交易引擎的批量订单请求(BatchOrder)将会按照id分批加载并发送。

批量发送

Ingress持续消费MQ的订单(ReceiveTask)并定序Sequence,满足以下任一条件时,将会将积攒的订单批量通过grpc发送到交易引擎(Engine):

  • 累计订单数超过200
  • 等待时间超过50ms

如发送失败,为了避免重复订单发送到Engine,Ingress将会重启,之后执行恢复撮合引擎状态的流程。

去重

Ingress维护一个去重LRU集合,size=10M个元素。每次接收到订单时,都会检查该订单的id是否已存在于去重集合中。

Engine

交易引擎,负责处理定序器发送的批量订单请求,进行撮合交易,并将撮合结果(OrderResult)、撮合动作(OrderAction)写入DB。

Engine同时将最新的Sequence通过消息队列发送到清算模块,唤醒清算模块进行后续处理。

Clearing

清算模块,负责加载并处理交易引擎存储的撮合结果,进行配额、余额、订单状态、流水、成交记录等的创建或修改,并将清算结果写入数据库。

清算模块将会生成如下内容:

golang
ClearingContext         *ClearingContext // 清算上下文,包含清算相关的上下文信息,如交易发生的时间等
BalanceChangesForTrades []*BalanceChange // 余额变化动作
OrderChanges            []*OrderChange   // 订单变化动作
TradeSplitsGenerated    map[string]*db.TradeSplit // 交易分拆记录
EventsGenerated         *EventCollection // 事件集合
CashFlow                []*db.CashFlow // 现金流记录
TradesGenerated         []*db.Trade // 成交记录
FeeRequests             []*FeeRequest // 手续费请求
CancelledOrders         []*db.CancelledOrder // 特殊撤单记录
QuotaChanges            []*QuotaChange // 配额变化记录

手续费

手续费的收取根据用户VIP等级和交易对的手续费率进行计算。清算模块会根据撮合结果生成手续费请求。

用户VIP等级通过UserAttributes模块获取,交易对的手续费率通过Currency配置获取。两项均有短期缓存。

手续费的清算是实时的,但结算是批量的。清算后,将生成FeeTodo记录存储到数据库中,等待定时任务(Cron)进行批量结算。

Cron

定时任务模块,负责处理清算模块生成的FeeTodo记录,批量结算手续费,并将手续费结算结果调用Balance模块进行更新。

Cron会定期从数据库获取待执行的FeeTodo记录,根据用户ID、币种ID合并余额变更动作,并调用Balance模块的MustModifyBalanceBatch方法进行批量更新。

更新成功之后,Cron会删除已处理的FeeTodo记录。

该动作为幂等动作,即使多次执行也不会影响最终结果。

Archive

归档模块,负责将清算模块生成的成交记录、现金流记录等数据进行归档处理。归档数据将会被存储到专门的归档数据库中,以便长期保存和查询。

归档对象

目前会对32天前的以下数据进行滚动归档:

  • OrderHistory:订单历史记录(仅归档status=[FILLED, CANCELLED]的记录)
  • UserRequest: 用户请求记录(仅归档sequence!=0的记录)
  • OrderResult:订单撮合记录
  • OrderAction:订单动作记录

仍需归档的数据包括:

  • Trade:成交记录
  • TradeSplit:交易分拆记录
  • CashFlow:现金流记录
  • TCCBarrierCaller:TCC调用方记录
  • CancelledOrder:特殊撤单记录

归档流程

  1. 以id分区查询,分批次处理,每页5000条,总计最多处理1000000条记录。
  2. 导出主库的数据到csv
  3. StreamLoad数据到StarRocks
  4. 重复1-3归档其它种类的数据
  5. 压缩本地CSV文件,为一个压缩包,上传S3
  6. 留下归档记录ArchiveHistory。
  7. 根据主键ID物理删除主库过期数据

注意

  • 归档时将会对StarRocks产生压力,StarRocks的主从同步可能会有延迟,因此归档时需要注意archive.check_interval的配置,控制同步节奏,同时监控CloudCanel的同步延迟图表。
    • archive.check_interval一般设置为15分钟,以平衡归档速度和StarRocks压力。
  • 归档后,数据库虽然删除了数据,但数据库占用的磁盘空间不会减少,如必要,进行数据库的Optimize Table降低物理占用。
  • 归档后,订单数据在接口层面做了适配,可以在一个API调用时同时查询主库和归档库的数据。

Quota

配额模块,负责验证用户的配额是否足够,锁定配额,并在清算时修改配额。该配额指的是用户只允许使用在KYC中声明的法币交易额度的30%进行购买加密货币。(详见Quota模块)

Balance

余额模块,负责验证用户的余额是否足够,锁定余额,并在清算时修改余额。该余额指的是用户在交易所的法币和加密货币的实际持有量。(详见Quota模块)

🚀 构建现代化数字资产交易平台