接入层设计:协议适配(FIX/FAST/REST)、会话管理、连接池

接入层,说白了就是报价路由系统的“大门”。

所有外部交易对手、流动性提供商,都得通过这扇门进来。我做过好几个报价系统,每次最头疼的都不是业务逻辑,而是接入层——因为外面来的“人”五花八门,有的说FIX,有的说FAST,有的走REST。你想想看,如果大门设计不好,后面再牛的路由算法也是白搭。

协议适配:让不同“方言”都能听懂

协议适配,就是给系统装一个“翻译官”。

我个人习惯把协议适配分成三层:传输层消息层业务层。传输层管TCP/HTTP怎么连,消息层管报文怎么解析,业务层管字段怎么映射。

FIX协议适配

FIX是金融圈的老大哥。几乎每个做交易系统的团队都绕不开它。

我在项目中遇到过一个问题:同一个FIX标签,不同对手方给的业务含义不一样。比如Tag 48(SecurityID),有的用ISIN,有的用Bloomberg Ticker。怎么办?我当时的做法是——在适配层做一套字段映射表,每个对手方一个配置。

// FIX消息解析示例(简化版)
public class FixMessageAdapter {
    private Map<String, FieldMapper> counterpartyMappings;
    
    public Quickfix.Message adapt(Quickfix.Message rawMsg, String counterpartyId) {
        FieldMapper mapper = counterpartyMappings.get(counterpartyId);
        // 根据对手方映射表,重新组装内部标准消息
        return mapper.map(rawMsg);
    }
}
我的经验:FIX会话层最好用QuickFIX/J或QuickFIX/Go这类成熟库。别自己手写会话层——我见过有人自己实现FIX序列号校验,结果漏了重置逻辑,生产上丢了一整天的报价数据。

FAST协议适配

FAST协议,说白了就是FIX的“压缩版”。它用模板+增量编码,把报文体积压到极致。高频交易场景下,FAST几乎是标配。

嗯,这里要注意:FAST的难点不在解析,在状态管理。因为FAST是增量编码,你得维护一个“前一条消息”的状态,才能解出完整数据。

// FAST解码器状态维护
public class FastDecoderSession {
    private Map<Integer, Object> previousValues; // 前序字段值
    
    public MarketDataEntry decode(byte[] raw, int templateId) {
        // 根据模板ID找到对应的解码模板
        FastTemplate template = templateRepository.get(templateId);
        // 解码时,如果字段是增量编码,需要结合previousValues
        return template.decode(raw, previousValues);
    }
}
我曾经踩过的坑:FAST会话断开重连后,如果不清除previousValues,解码出来的数据全是错的。后来我强制要求:每次重连,必须重新初始化解码器状态。

REST协议适配

REST接入,现在越来越多了。特别是那些做零售外汇、加密货币的对手方,基本都走REST。

我个人习惯把REST适配做成异步非阻塞的。因为报价是高频的,如果用同步阻塞,线程池很快就撑爆了。

// 基于WebFlux的REST适配器
@Component
public class RestQuoteAdapter {
    private final WebClient webClient;
    
    public Mono<Quote> fetchQuote(String symbol) {
        return webClient.get()
            .uri("/quotes/{symbol}", symbol)
            .retrieve()
            .bodyToMono(QuoteResponse.class)
            .map(this::toInternalQuote);
    }
}

会话管理:谁在说话,说了多久

会话管理,就是管“谁在连着我,连了多久,状态对不对”。

我见过最糟糕的设计:每个连接来了就开一个线程,线程里while(true)读数据。结果连接数一多,CPU全花在线程切换上了。

好的会话管理,我觉得要管三件事:

  • 会话生命周期:创建、认证、心跳、重连、销毁
  • 会话状态机:每个会话当前处于什么状态(已连接、已认证、已断开)
  • 会话元数据:对手方ID、协议类型、最后活动时间、序列号
// 会话状态机示例
public enum SessionState {
    DISCONNECTED,
    CONNECTING,
    CONNECTED,
    AUTHENTICATING,
    AUTHENTICATED,
    RECONNECTING,
    TERMINATED
}

public class Session {
    private String sessionId;
    private String counterpartyId;
    private ProtocolType protocol;
    private SessionState state;
    private long lastHeartbeat;
    private int nextSeqNum;
}
核心原则:会话状态变更必须走事件驱动。不要用if-else硬编码状态流转。我习惯用状态模式(State Pattern)或者有限状态机框架(如Spring Statemachine)。

连接池:别让连接成为瓶颈

连接池,说白了就是“复用连接,减少开销”。

但报价系统的连接池,跟普通数据库连接池不一样。报价连接是长连接,而且每个连接都有独立的会话状态。你不能随便把连接还给池子——因为会话状态还在呢。

我建议的做法是:

  • 按对手方分池:每个对手方一个连接池,互不干扰
  • 池内连接数动态调整:根据报价流量,自动扩缩容
  • 健康检查:定期发送心跳,踢掉死连接
// 连接池配置示例
public class ConnectionPoolConfig {
    private int minIdle = 2;        // 最小空闲连接
    private int maxTotal = 10;      // 最大总连接数
    private int maxWaitMillis = 100; // 获取连接超时(毫秒)
    private boolean testOnBorrow = true; // 借用时检查连接是否存活
    private long timeBetweenEvictionRuns = 5000; // 5秒一次清理
}
我的经验:连接池的“最大连接数”不要设太大。我见过有人设了100,结果对手方那边限流了,这边还在拼命建连,最后全被拒。我一般设10-20,配合背压机制,够了。

整体架构图

下面这张图,是我做接入层设计时常用的分层结构。你可以看到,协议适配在最外层,会话管理在中间,连接池在最底层。三者配合,才能支撑起高并发的报价接入。

接入层架构设计 协议适配层 FIX 协议适配 FAST 协议适配 REST 协议适配 会话管理层 会话生命周期管理 会话状态机 会话元数据管理 连接池层 按对手方分池 动态扩缩容 健康检查与清理

避坑指南

最后,分享几个我实际项目中踩过的坑:

  • 协议版本不兼容:FIX 4.2和FIX 4.4的字段定义不一样。我曾经因为没做版本检测,导致解析出来的价格多了两位小数,亏了不少钱。
  • 会话超时设置:心跳超时设太短,网络抖动就断连;设太长,死连接占着资源。我一般设30秒心跳,90秒超时。
  • 连接池泄漏:有一次忘记在finally里归还连接,结果池子慢慢被耗尽,系统静默挂了。后来我强制要求:所有连接操作必须用try-with-resources。
总结一下:接入层设计,核心就是“适配、管理、复用”。协议适配解决“怎么听懂”,会话管理解决“谁在说话”,连接池解决“怎么高效说话”。三者缺一不可。
专注资料整理