接入层设计:协议适配(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);
}
}
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);
}
}
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;
}
连接池:别让连接成为瓶颈
连接池,说白了就是“复用连接,减少开销”。
但报价系统的连接池,跟普通数据库连接池不一样。报价连接是长连接,而且每个连接都有独立的会话状态。你不能随便把连接还给池子——因为会话状态还在呢。
我建议的做法是:
- 按对手方分池:每个对手方一个连接池,互不干扰
- 池内连接数动态调整:根据报价流量,自动扩缩容
- 健康检查:定期发送心跳,踢掉死连接
// 连接池配置示例
public class ConnectionPoolConfig {
private int minIdle = 2; // 最小空闲连接
private int maxTotal = 10; // 最大总连接数
private int maxWaitMillis = 100; // 获取连接超时(毫秒)
private boolean testOnBorrow = true; // 借用时检查连接是否存活
private long timeBetweenEvictionRuns = 5000; // 5秒一次清理
}
整体架构图
下面这张图,是我做接入层设计时常用的分层结构。你可以看到,协议适配在最外层,会话管理在中间,连接池在最底层。三者配合,才能支撑起高并发的报价接入。
避坑指南
最后,分享几个我实际项目中踩过的坑:
- 协议版本不兼容:FIX 4.2和FIX 4.4的字段定义不一样。我曾经因为没做版本检测,导致解析出来的价格多了两位小数,亏了不少钱。
- 会话超时设置:心跳超时设太短,网络抖动就断连;设太长,死连接占着资源。我一般设30秒心跳,90秒超时。
- 连接池泄漏:有一次忘记在finally里归还连接,结果池子慢慢被耗尽,系统静默挂了。后来我强制要求:所有连接操作必须用try-with-resources。