技术博客

成交回报与确认:回报流水的幂等设计

第 32 / 141 篇 · 2025-12-21 · secfin · 成交回报幂等设计状态机

回报是交易的另一半。委托报出去只是开始,资金和持仓的变动要等成交回报回来才算数。而回报链路——交易所到会员、会员到柜台、柜台到客户端——每一跳都可能把同一条成交记录送两次。这篇讲为什么"恰好一次"在这个链路里是数学上就立不住的目标,幂等消费怎么落地,以及回报处理和委托状态机之间必须守住的不变量。

为什么回报必然重复

三个结构性原因,哪个都消灭不掉:

  1. 传输层的尽力而为:交易所回报常用 UDP 组播推送,丢了不补,但重复很常见——同一帧被两台网关收到、网关重启后重放缓存,都会变成重复回报;
  2. 断线重连的补偿:柜台与交易所的会话断掉再连上,标准做法是按回报序号补拉缺口。补拉窗口和实时推送窗口必然重叠,重叠区就是重复区;
  3. 故障切换的代价:柜台主备切换时,备机不确定最后几条回报对端是否收到,宁可重发——重复总比丢失好,这是全链路各个环节的共同倾向。

结论:链路语义是 at-least-once。在不可靠信道加节点故障的模型下,exactly-once 要求无限期保留去重状态,没人付得起这个成本。所以正确的姿势是一句话:允许重复送达,保证重复无效。

幂等键:回报里必须有的两个号码

一条成交回报最少要携带两个能唯一定位它的字段:

  • 委托号:柜台侧订单标识,断线重报后新旧会话靠它对齐;
  • 成交号:交易所撮合时分配的全局唯一号,同一次成交无论重放多少次,成交号不变。

幂等键就是 $(\text{委托号}, \text{成交号})$ 这个二元组,落地是数据库里的一条唯一约束:

CREATE TABLE exec_report (
    order_id   VARCHAR(32) NOT NULL,
    exec_id    VARCHAR(32) NOT NULL,   -- 交易所成交号
    last_qty   DECIMAL(18,2) NOT NULL, -- 本次成交量
    last_px    DECIMAL(18,4) NOT NULL, -- 本次成交价
    report_seq BIGINT NOT NULL,        -- 会话内单调递增序号
    recv_time  TIMESTAMP NOT NULL,
    PRIMARY KEY (order_id, exec_id)
);

重复回报到达时主键冲突,捕获后写一张重复回报日志就丢弃。注意日志必须记:重复回报本身是异常信号,某客户的重复率突然飙升,先查他家网关是不是出了问题,而不是静默吞掉。

序号 report_seq 是第二道防线,消费时计算缺口:

$$ \text{gap} = \text{seq}_{\text{expected}} - \text{seq}_{\text{received}} $$

  • gap > 0:乱序到达,进暂存窗口等前面的到齐;
  • gap < 0:丢包,触发按序号补拉;
  • 暂存窗口有容量上限(一般几百条),超了说明对端状态已经乱了,降级报警走人工。

幂等键解决"同一条来两次",序号解决"中间少了一条",两个机制正交,缺一不可。

部分成交的累计不变量

回报会驱动资金解冻,处理逻辑里要维护一条硬不变量:累计成交量 ≤ 委托量,以及终态等式:

$$ \text{累计成交} + \text{累计撤单} = \text{委托量} $$

部成回报到达时做累计,一旦累计成交超过委托量,要么收到了假回报,要么本地委托量被改过——这是脏数据,直接熔断这条委托的后续处理,让日终对账来收口。宁可停一条委托,不能错一分钱。反过来,交易所发"全成"回报时累计量应该恰好等于委托量,差一点都要查:残量去哪了?是不是还有一条部成在乱序窗口里躺着?

和状态机的咬合:只有回报能驱动迁移

撤单篇立过规矩:迁移即账务,账务随确认。落到成交上具体是三条纪律:

  • 柜台严禁用"我算出来应该会成交"提前改状态。预扣资金只是冻结,部成回报回来才真正记减少;
  • 全成确认之前,剩余量保持冻结,因为"部成 → 撤残量"的组合随时可能出现;
  • 回报处理必须在委托粒度上串行化:同一委托的两条部成并发处理,会打出错序的余额流水。做法是把回报按 order_id 散列到固定分区的消费线程,保证单委托内严格有序。

吞吐上有个常见误区:去重要不要每条查库?热点时段回报十万级 TPS,每条都 SELECT 一次去重表,数据库会先被打穿。做法是内存哈希集合做一级去重(进程重启后从去重表重建),落库走批量异步,主键约束兜底最后一道防线。和撮合一样,在这条链路上数据库是审计层,不是热路径。

回报链路语义是 at-least-once:允许重发,保证重复无效交易所撮合生成成交号回报网关UDP / 重连补拉柜台回报线程按序号消费幂等检查(委托号, 成交号)去重键:委托号 + 交易所成交号(唯一索引)主键冲突即重复回报:记日志后丢弃序号缺口 gap = expected - received:为正乱序暂存,为负丢包补拉不变量:累计成交 ≤ 委托量;终态时 累计成交 + 累计撤单 = 委托量内存哈希做一级去重,库表主键兜底;重复率异常本身是故障信号

最后一条纪律:对账先于信任。柜台内存里的成交明细,日终必须和交易所下发的成交对账文件逐笔核对。幂等保证"不多",对账保证"不少"——交易系统里所有关于"不少"的承诺,最终都落在对账上。