Spot交易核心模块:下单、定序、清算、归档
总体流程图
Core
收集所有API端的请求,并将其转发到Order模块进行处理。
Order
前置收单模块,负责处理用户的下单请求,包括验证配额和余额是否足够,锁定配额和余额,整理用户请求参数,并将请求通过MQ转发到定序器。
前置校验
Order校验一系列条件:
- 用户配额是否足够(Quota)
- 用户余额是否足够(Balance)
- 提交的订单参数是否正确且符合交易限制(如价格、数量等)
- 用户是否被禁止交易(如黑名单等)
- 用户是否已通过KYC(Know Your Customer)认证
- 用户是否超过最大挂单量限制
- 用户提交的客户端订单号是否重复
- 系统是否无法处理更多订单(如系统负载过高)
- 该币种是否正在交易
Order模块将会规整用户的下单类型,具体包括:
| No | Type | OrderSide | QuantityMode | Price | BTC Amount | USDT Amount | Value | Price | Amount | Backend Behavior | Lock | BuyOrSell | ShareOrVolume | MarketOrLimit | LimitPrice | Shares | MarketVolume |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | LIMIT | BUY | AMOUNT | 666 | 1 | 666 | 1 | 666 USDT | BUY | Share | LIMIT | 666 | 1 | ||||
| 2 | LIMIT | BUY | VALUE | 666 | 10000 | 666 | 10000/666 | to AMOUNT | 10000/666*666 | BUY | Share | LIMIT | 10000/666 | ||||
| 3 | MARKET | BUY | AMOUNT | 1 | 1 * Ask1 * 1.05 | 1 | to VALUE | 1 * Ask1 * 1.05 | BUY | Volume | MARKET | 1 | 1 * Ask1 * 1.05 | ||||
| 4 | MARKET | BUY | VALUE | 10000 | 10000 | 10000 USDT | BUY | Volume | MARKET | 10000 | |||||||
| 5 | LIMIT | SELL | AMOUNT | 666 | 1 | 666 | 1 | 1 BTC | SELL | Share | LIMIT | 666 | 1 | ||||
| 6 | LIMIT | SELL | VALUE | 666 | 10000 | 666 | 10000/666 | to AMOUNT | 10000/666 BTC | SELL | Share | LIMIT | 666 | 10000/666 | |||
| 7 | MARKET | SELL | AMOUNT | 1 | 1 | 1 BTC | SELL | Share | MARKET | 1 | |||||||
| 8 | MARKET | SELL | VALUE | 10000 | 10000/Bid1/1 | to AMOUNT | 10000/Bid1/1 BTC | SELL | Share | MARKET | 10000/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
清算模块,负责加载并处理交易引擎存储的撮合结果,进行配额、余额、订单状态、流水、成交记录等的创建或修改,并将清算结果写入数据库。
清算模块将会生成如下内容:
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:特殊撤单记录
归档流程
- 以id分区查询,分批次处理,每页5000条,总计最多处理1000000条记录。
- 导出主库的数据到csv
- StreamLoad数据到StarRocks
- 重复1-3归档其它种类的数据
- 压缩本地CSV文件,为一个压缩包,上传S3
- 留下归档记录ArchiveHistory。
- 根据主键ID物理删除主库过期数据
注意
- 归档时将会对StarRocks产生压力,StarRocks的主从同步可能会有延迟,因此归档时需要注意archive.check_interval的配置,控制同步节奏,同时监控CloudCanel的同步延迟图表。
- archive.check_interval一般设置为15分钟,以平衡归档速度和StarRocks压力。
- 归档后,数据库虽然删除了数据,但数据库占用的磁盘空间不会减少,如必要,进行数据库的Optimize Table降低物理占用。
- 归档后,订单数据在接口层面做了适配,可以在一个API调用时同时查询主库和归档库的数据。
Quota
配额模块,负责验证用户的配额是否足够,锁定配额,并在清算时修改配额。该配额指的是用户只允许使用在KYC中声明的法币交易额度的30%进行购买加密货币。(详见Quota模块)
Balance
余额模块,负责验证用户的余额是否足够,锁定余额,并在清算时修改余额。该余额指的是用户在交易所的法币和加密货币的实际持有量。(详见Quota模块)
