1. 简介

在数据服务领域,效率和性能是两大核心要素,尤其是在处理高吞吐量数据流时。显然,采用高性能编码技术减少消息体积是关键所在。

然而,自研编解码算法往往既繁琐又脆弱,长期维护成本极高。

幸运的是,Simple Binary Encoding(SBE)能帮助我们以实用方式实现和维护定制化的编解码系统。

本文将深入探讨SBE的核心价值,并通过代码示例演示其使用方法。

2. 什么是SBE?

SBE是一种专为低延迟数据流设计的二进制编码格式。它也是FIX SBE标准的参考实现,该标准专用于金融数据编码。

2.1 消息结构

为保持流式语义,消息必须支持顺序读写,禁止回溯操作。这种设计消除了指针解引用、位置指针管理等额外操作,能更好地利用硬件支持,实现极致性能和效率。

SBE的消息结构如下:sbe消息结构

  • 消息头:包含版本号等必填字段,可根据需要扩展
  • 根字段:消息的静态字段,块大小预定义且不可变,可设为可选
  • 重复组:表示集合类型数据,可包含字段和嵌套组以构建复杂结构
  • 可变数据字段:无法预知大小的字段(如字符串、二进制数据),位于消息末尾

接下来分析这种结构设计的必要性。

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>。先看类型定义:

  1. 消息头(必填):

    <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:模式版本号
  2. 交易所枚举

    <enum name="Market" encodingType="uint8">
        <validValue name="NYSE" description="纽约证券交易所">0</validValue>
        <validValue name="NASDAQ" description="纳斯达克">1</validValue>
    </enum>
    

    ✅ 使用uint8编码,支持256个交易所(0-255)

  3. 股票代码

    <type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII" description="股票代码"/>
    

    ✅ 限制为4位ASCII字符(7位编码,最大128字符)

  4. 十进制数

    <composite name="Decimal">
        <type name="mantissa" primitiveType="uint64" minValue="0"/>
        <type name="exponent" primitiveType="int8"/>
    </composite>
    
    • mantissa:有效数字
    • exponent:小数位数(如mantissa=98765, exponent=-3 → 98.765)
  5. 货币枚举

    <enum name="Currency" encodingType="uint8">
        <validValue name="USD" description="美元">0</validValue>
        <validValue name="EUR" description="欧元">1</validValue>
    </enum>
    
  6. 报价复合类型

    <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的核心概念,演示了:

  1. 通过XML定义消息模式
  2. 生成Java存根类
  3. 实现消息编解码

SBE在金融数据等固定长度数据场景表现优异,但在处理可变长度数据时需谨慎设计。完整代码示例请参考GitHub仓库


原始标题:Guide to Simple Binary Encoding | Baeldung