1. 简介
在数据服务领域,效率和性能是两大核心要素,尤其是在处理高吞吐量数据流时。显然,采用高性能编码技术减少消息体积是关键所在。
然而,自研编解码算法往往既繁琐又脆弱,长期维护成本极高。
幸运的是,Simple Binary Encoding(SBE)能帮助我们以实用方式实现和维护定制化的编解码系统。
本文将深入探讨SBE的核心价值,并通过代码示例演示其使用方法。
2. 什么是SBE?
SBE是一种专为低延迟数据流设计的二进制编码格式。它也是FIX SBE标准的参考实现,该标准专用于金融数据编码。
2.1 消息结构
为保持流式语义,消息必须支持顺序读写,禁止回溯操作。这种设计消除了指针解引用、位置指针管理等额外操作,能更好地利用硬件支持,实现极致性能和效率。
- 消息头:包含版本号等必填字段,可根据需要扩展
- 根字段:消息的静态字段,块大小预定义且不可变,可设为可选
- 重复组:表示集合类型数据,可包含字段和嵌套组以构建复杂结构
- 可变数据字段:无法预知大小的字段(如字符串、二进制数据),位于消息末尾
接下来分析这种结构设计的必要性。
2.2 SBE的适用场景
SBE的威力源于其消息结构——专为顺序访问优化。因此,SBE特别适合数值、位集、枚举和数组等固定大小数据。
典型应用场景是金融数据流(主要包含数值和枚举),这正是SBE的设计初衷。
但SBE并不擅长处理字符串、二进制数据等可变长度类型。原因在于我们通常无法预知数据大小,导致流式处理时需要额外计算确定数据边界。在毫秒级延迟敏感场景中,这会成为性能瓶颈。
尽管SBE仍支持String和Blob类型,但它们始终位于消息末尾,以最大限度减少可变长度计算的影响。
3. 库配置
使用SBE库需在pom.xml中添加以下Maven依赖:
<dependency>
<groupId>uk.co.real-logic</groupId>
<artifactId>sbe-all</artifactId>
<version>1.27.0</version>
</dependency>
4. 生成Java存根类
生成存根类前,需先定义消息模式。SBE支持通过XML定义模式。
下面演示如何为市场交易数据定义消息模式。
4.1 创建消息模式
基于FIX协议的XSD规范创建XML模式文件:
<?xml version="1.0" encoding="UTF-8"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
package="com.baeldung.sbe.stub" id="1" version="0" semanticVersion="5.2"
description="股票市场数据模式">
<types>
<composite name="messageHeader"
description="消息标识符和根长度">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<enum name="Market" encodingType="uint8">
<validValue name="NYSE" description="纽约证券交易所">0</validValue>
<validValue name="NASDAQ"
description="纳斯达克">1</validValue>
</enum>
<type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII"
description="股票代码"/>
<composite name="Decimal">
<type name="mantissa" primitiveType="uint64" minValue="0"/>
<type name="exponent" primitiveType="int8"/>
</composite>
<enum name="Currency" encodingType="uint8">
<validValue name="USD" description="美元">0</validValue>
<validValue name="EUR" description="欧元">1</validValue>
</enum>
<composite name="Quote"
description="股票市场报价">
<ref name="market" type="Market"/>
<ref name="symbol" type="Symbol"/>
<ref name="price" type="Decimal"/>
<ref name="currency" type="Currency"/>
</composite>
</types>
<sbe:message name="TradeData" id="1" description="交易报价及数量">
<field name="quote" id="1" type="Quote"/>
<field name="amount" id="2" type="uint16"/>
</sbe:message>
</sbe:messageSchema>
模式包含两个核心部分:<types>
和<sbe:message>
。先看类型定义:
消息头(必填):
<composite name="messageHeader" description="消息标识符和根长度"> <type name="blockLength" primitiveType="uint16"/> <type name="templateId" primitiveType="uint16"/> <type name="schemaId" primitiveType="uint16"/> <type name="version" primitiveType="uint16"/> </composite>
blockLength
:根字段预留空间(不含重复组和可变字段)templateId
:消息模板标识符schemaId
:模式标识符version
:模式版本号
交易所枚举:
<enum name="Market" encodingType="uint8"> <validValue name="NYSE" description="纽约证券交易所">0</validValue> <validValue name="NASDAQ" description="纳斯达克">1</validValue> </enum>
✅ 使用
uint8
编码,支持256个交易所(0-255)股票代码:
<type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII" description="股票代码"/>
✅ 限制为4位ASCII字符(7位编码,最大128字符)
十进制数:
<composite name="Decimal"> <type name="mantissa" primitiveType="uint64" minValue="0"/> <type name="exponent" primitiveType="int8"/> </composite>
mantissa
:有效数字exponent
:小数位数(如mantissa=98765, exponent=-3 → 98.765)
货币枚举:
<enum name="Currency" encodingType="uint8"> <validValue name="USD" description="美元">0</validValue> <validValue name="EUR" description="欧元">1</validValue> </enum>
报价复合类型:
<composite name="Quote" description="股票市场报价"> <ref name="market" type="Market"/> <ref name="symbol" type="Symbol"/> <ref name="price" type="Decimal"/> <ref name="currency" type="Currency"/> </composite>
最后定义消息结构:
<sbe:message name="TradeData" id="1" description="交易报价及数量">
<field name="quote" id="1" type="Quote"/>
<field name="amount" id="2" type="uint16"/>
</sbe:message>
⚠️ 更多类型细节请参考官方规范
4.2 使用SbeTool生成代码
通过SBE jar文件生成Java存根类:
java -jar -Dsbe.output.dir=target/generated-sources/java
~/.m2/repository/uk/co/real-logic/sbe-all/1.27.0/sbe-all-1.27.0.jar
src/main/resources/schema.xml
💡 提示:将
~/.m2
替换为你的本地Maven仓库路径
成功后代码生成在target/generated-sources/java
目录。
4.3 集成Maven插件
更优雅的方式是集成到Maven构建流程:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>uk.co.real_logic.sbe.SbeTool</mainClass>
<systemProperties>
<systemProperty>
<key>sbe.output.dir</key>
<value>${project.build.directory}/generated-sources/java</value>
</systemProperty>
</systemProperties>
<arguments>
<argument>${project.basedir}/src/main/resources/schema.xml</argument>
</arguments>
</configuration>
<dependencies>
<dependency>
<groupId>uk.co.real-logic</groupId>
<artifactId>sbe-tool</artifactId>
<version>1.27.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/java/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
✅ 执行mvn clean install
时自动生成代码
5. 基础消息处理
准备好Java存根类后,演示实际使用。
5.1 准备测试数据
创建测试数据类:
public class MarketData {
private int amount;
private double price;
private Market market;
private Currency currency;
private String symbol;
// 构造方法/getter/setter
}
初始化测试数据:
private MarketData marketData;
@BeforeEach
public void setup() {
marketData = new MarketData(2, 128.99, Market.NYSE, Currency.USD, "IBM");
}
5.2 消息编码
创建编码器和缓冲区:
@Test
public void givenMarketData_whenEncode_thenDecodedValuesMatch() {
UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocate(128));
MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder();
TradeDataEncoder dataEncoder = new TradeDataEncoder();
// 价格转换(BigDecimal避免精度丢失)
BigDecimal priceDecimal = BigDecimal.valueOf(marketData.getPrice());
int priceMantissa = priceDecimal.scaleByPowerOfTen(priceDecimal.scale()).intValue();
int priceExponent = priceDecimal.scale() * -1;
// 编码消息
TradeDataEncoder encoder = dataEncoder.wrapAndApplyHeader(buffer, 0, headerEncoder);
encoder.amount(marketData.getAmount());
encoder.quote()
.market(marketData.getMarket())
.currency(marketData.getCurrency())
.symbol(marketData.getSymbol())
.price()
.mantissa(priceMantissa)
.exponent((byte) priceExponent);
}
⚠️ 货币计算务必使用BigDecimal,避免精度丢失!
5.3 消息解码
创建解码器并读取数据:
MessageHeaderDecoder headerDecoder = new MessageHeaderDecoder();
TradeDataDecoder dataDecoder = new TradeDataDecoder();
// 解码消息头
dataDecoder.wrapAndApplyHeader(buffer, 0, headerDecoder);
// 价格重构
double price = BigDecimal.valueOf(dataDecoder.quote().price().mantissa())
.scaleByPowerOfTen(dataDecoder.quote().price().exponent())
.doubleValue();
// 验证数据
Assertions.assertEquals(2, dataDecoder.amount());
Assertions.assertEquals("IBM", dataDecoder.quote().symbol());
Assertions.assertEquals(Market.NYSE, dataDecoder.quote().market());
Assertions.assertEquals(Currency.USD, dataDecoder.quote().currency());
Assertions.assertEquals(128.99, price);
6. 总结
本文介绍了SBE的核心概念,演示了:
- 通过XML定义消息模式
- 生成Java存根类
- 实现消息编解码
SBE在金融数据等固定长度数据场景表现优异,但在处理可变长度数据时需谨慎设计。完整代码示例请参考GitHub仓库。