专栏文章 高性能序列化工具 ChronicleWire

FunTester · 2024年01月22日 · 最后由 FunTester 回复于 2024年01月24日 · 6092 次阅读

之前使用 chronicle 进行过日志回放框架的设计,效果很不错。后面在更加深入了解过程中,发现 chronicle 性能优势中一个非常重要的方面,就是序列化和反序列化。chronicle 提供了多个功能类,实现不同格式数据的序列化和反序列化功能。

Java 序列化是一种流行的机制,用于对复杂对象图进行序列化和反序列化。丰富的功能通常伴随着性能的牺牲。如果需求不包括对这些类型的递归图进行序列化,那么可以考虑使用开源解决方案 ChronicleWire。它通过简化结构并采用树状结构显著提高了性能。此外,ChronicleWire 支持多种格式,而无需修改代码即可切换。本文将介绍序列化的基础知识,并讨论 ChronicleWire 的一些关键优势。

在 Java 中,序列化是将对象转换为字节流,以便将其存储在文件中或通过网络传输。然后,可以将字节流重新反序列化为对象,恢复其原始状态。然而,Java 默认的序列化机制在处理包含相互引用的复杂对象图时可能导致性能问题。

ChronicleWire 通过采用更为简单的树状结构来避免这些性能问题。它不仅提供了更高的效率,还减少了序列化的复杂性。此外,ChronicleWire 支持多种数据格式,包括二进制、文本和 JSON,使得可以轻松切换而无需修改代码。

在实际应用中,ChronicleWire 的性能提升尤为显著,特别是在处理大量数据或对低延迟要求极高的场景下。通过使用 ChronicleWire,您可以更加灵活地平衡性能和复杂性,根据实际需求选择最合适的序列化方案。

本文探讨了 Java 序列化的基础知识,并介绍了 ChronicleWire 作为一个优秀的开源解决方案,可以在提高性能的同时降低序列化复杂性。这使得 ChronicleWire 成为处理大规模数据和对性能敏感的 Java 应用程序的理想选择。

序列化和反序列化

序列化涉及将 Java 对象编码为字节流。例如,当我们有一个保存应用程序状态的对象时,如果我们关闭应用程序,状态将丢失。为了避免这种情况,我们可以首先将应用程序的状态序列化并存储到磁盘上,将对象转换为字节形式,以便轻松存储。同样,如果我们希望通过网络发送 Java 对象中的数据,我们需要先将对象序列化,然后将其写入 TCP/IP 缓冲区。序列化的过程是将对象转换为字节流的表示形式,以便于存储或传输。

相反,反序列化是从字节开始,然后重新创建对象实例。这意味着我们可以通过读取存储的字节流或接收的网络数据,将其反序列化为原始的 Java 对象。这种过程允许我们在不丢失信息的情况下重新构建对象,使得数据的传输和持久化变得更为便利。

总的来说,序列化和反序列化是在 Java 中处理对象存储、传输和持久化的关键机制。序列化将对象转换为字节流,便于存储和传输,而反序列化允许从字节流中重新构建原始对象。这一过程在应用程序状态的保存、网络通信和数据持久化等方面发挥着重要作用。

关于 Chronicle Wire

ChronicleWire是一个开源库,最初设计用于支持ChronicleQueueChronicleMap,但在任何需要序列化的代码中都表现出色。与原生 Java 序列化的不同之处在于,ChronicleWire实际上支持多种不同的格式,包括二进制、YAML、JSON、原始二进制数据和 CSV。ChronicleWire的真正创新之处在于,您无需更改代码即可更改编码方式。

该库将序列化的实现抽象为可插入的 Wire 实现。其核心理念是,对象只需描述要序列化的内容,而不需要描述如何序列化。这一理念通过实现Marshallable接口的对象(即要序列化的 POJO)得以实现。在 Java 序列化中,您可以通过在类上添加java.io.Serializable标记接口来指示对象可以进行序列化,而在ChronicleWire中,对象需要实现net.openhft.chronicle.wire.Marshallable接口。

通过采用这种抽象和接口实现,ChronicleWire为开发人员提供了更大的灵活性和可扩展性,使得可以轻松切换序列化格式,而无需对代码进行繁琐的修改。这使得在不同的应用场景中选择最合适的序列化方式变得更加简便。

编码

我们已经提到 Java 序列化将对象编码为二进制格式,而ChronicleWire则支持多种不同的编码格式。编码的选择直接影响存储数据所需的字节数,紧凑的格式通常意味着使用更少的字节。ChronicleWire在平衡格式的紧凑性的同时避免了对数据的压缩,因为压缩操作可能消耗宝贵的 CPU 时间。该库的目标是在保持灵活性和向后兼容性的同时提供高性能。

在 ChronicleWire 中,数据存储在尽可能少的字节中,而不损害性能。例如,采用停止位编码来存储整数。停止位编码是一种对整数进行紧凑表示的方法,通过将整数的每个字节的最高位(stop bit)用作标志,指示是否还有下一个字节。这种方式有效地减少了整数所占的字节数,提高了存储效率。

通过在选择编码格式时权衡紧凑性和性能,ChronicleWire 使得开发人员可以在不同的应用场景中取得最佳效果。其设计目标是提供一种灵活、高性能的序列化方式,同时保持与过去版本的向后兼容性。这使得 ChronicleWire 成为处理大规模数据、低延迟要求的应用程序的理想选择。

不同格式

ChronicleWire 提供多种实现,每一种都适用于不同的场景。例如,在需要提供应用程序配置文件或创建数据驱动测试的情况下,我们通常希望将对象序列化或反序列化为人类可读的格式,如 YAML 或 JSON。此外,ChronicleWire 还支持将 Java 对象序列化为类型化的 JSON,使得可以在应用程序的JavaScript UI 层发送和接收消息。

在实际应用中,能够在不同的编码格式之间进行互操作变得非常重要。以 Chronicle Queue 为例,它使用 ChronicleWire 的紧凑二进制格式存储数据。然后,Chronicle Queue 可以读取二进制数据,并以人类可读的 YAML 格式将记录输出到控制台。这种能力对于调试或生成符合遵从性报告非常有用,因为它允许开发人员轻松地查看存储的数据,并以易读的格式输出。

ChronicleWire 的这种多格式支持和互操作性使其在处理不同应用场景和需求的同时保持灵活性。无论是需要紧凑的二进制格式用于高性能的数据存储,还是人类可读的格式用于配置文件和测试,ChronicleWire 都提供了多样化的选择。这使得它成为一个强大而适应性强的序列化库。

使用案例

让我们看一个例子,ChronicleWire 将数据编码为简单字符串形式。我们使用以下案例:

import net.openhft.chronicle.bytes.Bytes  
import net.openhft.chronicle.core.pool.ClassAliasPool  
import net.openhft.chronicle.wire.Marshallable  
import net.openhft.chronicle.wire.Wire  
import net.openhft.chronicle.wire.YamlWire  

/**  
 * chronicle-queue,wire 案例  
 */  
class WireDemo {  

    static class Tester implements Marshallable {  

        int age  

        String name  

        Tester(String name, int age) {  
            this.name = name  
            this.age = age  
        }  
    }  

    static void main(String... args) {  
        ClassAliasPool.CLASS_ALIASES.addAlias(Tester.class)//允许 YAML 引用 Tester,而不是 net.openhft.chronicle.wire.WireDemo$Tester        
        Wire wire = new YamlWire(Bytes.allocateElasticOnHeap())//YamlWire 用于 YAML 格式的数据  
        wire.getValueOut().object(new Tester("FunTester", 18))//将对象写入 wire        System.out.println(wire)//打印 wire    }  

}

控制台输出:

!Tester {
  number: 18,
  driver: FunTester
}

如果我们换成 JSONWire,如下:

Wire wire = new JSONWire(Bytes.allocateElasticOnHeap())// 创建一个 JSONWire

如果我们希望 JSON 也包含 Java 类型,那么我们还可以如下设置:

Wire wire = new JSONWire(Bytes.allocateElasticOnHeap()).useTypes(true)// 创建一个 JSONWire,使用类型

这也将编码 java 类型 Tester:

{"@Tester":{"age":18,"name":"FunTester"}}

二进制格式

让我们继续一个使用紧凑二进制格式的示例:

static void main(String[] args) {  
    ClassAliasPool.CLASS_ALIASES.addAlias(Tester.class)  
    Wire wire = WireType.FIELDLESS_BINARY.apply(Bytes.allocateElasticOnHeap())  
    wire.getValueOut().object(new Tester("FunTester", 18))  
    System.out.println(wire.bytes().toHexString())  
}

其输出如下:

00000000 b6 06 54 65 73 74 65 72  82 0c 00 00 00 a1 12 e9 ··Tester ········
00000010 46 75 6e 54 65 73 74 65  72                      FunTeste r       

实例反序列化

到目前为止,所有的例子都涵盖了序列化,所以当涉及到序列化时,我们可以从数据开始,例如:

{"@Car":{"number":44,"driver":"Lewis Hamilton"}}

然后我们可以将这个 JSON 转换回 JAVA 对象:


static void main(String[] args) {  
    ClassAliasPool.CLASS_ALIASES.addAlias(Tester.class) // 注册 Tester 类的别名  
    Wire wire = new JSONWire().useTypes(true)// 创建 JSONWire 实例并启用类型信息  
    wire.bytes().append("{\"@Tester\":{\"age\":18,\"name\":\"FunTester\"}}") // 在 Wire 中追加 JSON 格式的字符串  
    Object object = wire.getValueIn().object()// 从 Wire 中获取对象  
    println object.getClass().getName() // 输出对象的类型  
    def tester = (Tester) object // 将对象转换为 Tester 类型  
    println tester.name  
    println tester.age  
}

控制台输出:

com.funtest.queue.WireDemo$Tester
FunTester
18

兼容性

如果字段名被编码,如果我们改变对象属性,包括int height(见下面的例子),这些数值将只是默认为零,当重新定义发生时,该字段将像往常一样加载。

import net.openhft.chronicle.core.pool.ClassAliasPool  
import net.openhft.chronicle.wire.JSONWire  
import net.openhft.chronicle.wire.Marshallable  
import net.openhft.chronicle.wire.Wire  

/**  
 * chronicle-queue,wire 案例  
 */  
class WireDemo {  

    static class Tester implements Marshallable {  

        int age  

        String name  

        int height  

        Tester(String name, int age) {  
            this.name = name  
            this.age = age  
        }  
    }  

    static void main(String[] args) {  
        ClassAliasPool.CLASS_ALIASES.addAlias(Tester.class) // 注册 Tester 类的别名  
        Wire wire = new JSONWire().useTypes(true)// 创建 JSONWire 实例并启用类型信息  
        wire.bytes().append("{\"@Tester\":{\"age\":18,\"name\":\"FunTester\"}}") // 在 Wire 中追加 JSON 格式的字符串  
        Object object = wire.getValueIn().object()// 从 Wire 中获取对象  
        println object.getClass().getName() // 输出对象的类型  
        def tester = (Tester) object // 将对象转换为 Tester 类型  
        println tester.name  
        println tester.age  
        println tester.height  
    }  

}

控制台打印如下:

com.funtest.queue.WireDemo$Tester
FunTester
18
0

字符串

通常,字符串使用 UTF8 标准编码,然而,字符串也可以使用Base Encoder编码,例如 Base64 编码器,它可以将数据存储到更紧凑的字符串或原语字段中。每个字节有 256 种不同的组合(这是因为一个字节由 8 位组成,位是 0 或 1,给出 28 个组合,因此是 256),然而,如果我们选择使用基本编码器,并假设我们可以将字符串限制为以下字符 “.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+” 这是一些最常用的字符,那么我们可以使用这个基本编码器将上面的 10 个字符存储到 8 个字节中。

我们可以创建自己的基本编码,它不只是必须包含这个数量的字符。使用更少的字符,可以从更大的紧凑性中受益。如前所述,数据越紧凑,读写速度就越快。

下面是一个 Chronicle Wire 如何将小字符串存储在长字符串中的例子,YAML 序列化器显示了字符串表示,但字符串仅使用 8 字节长存储在对象中,同样,二进制序列化器将使用更紧凑的 8 字节长表示。

static class FunText extends SelfDescribingMarshallable {  

    transient StringBuilder temp = new StringBuilder()  

    @LongConversion(Base64LongConverter.class)  
    long text  

    FunText(CharSequence text) {  
        this.text = Base64LongConverter.INSTANCE.parse(text)  
    }  

    CharSequence text() {  
        Base64LongConverter.INSTANCE.append(temp, text)  
        return temp  
    }  

}  

static void main(String[] args) {  
    ClassAliasPool.CLASS_ALIASES.addAlias(FunText.class)//注册别名  
    Wire wire = new BinaryWire(Bytes.allocateElasticOnHeap())//创建wire,默认是二进制的  
    wire.getValueOut().object(new FunText("FunTester"))//序列化  
    System.out.println("序列化: " + wire.bytes().toHexString())//打印序列化后的字节码  
    System.out.println("反序列化: " + wire.getValueIn().object())//反序列化  
}

控制台输出:

序列化: 00000000 b6 07 46 75 6e 54 65 78  74 82 0e 00 00 00 c4 74 ··FunTex t······t
00000010 65 78 74 a7 ec e7 b6 1f  85 be 06 00             ext····· ····    

反序列化: !FunText {
  text: FunTester
}

结论

Chronicle Wire 允许您将对象序列化为二进制格式和从二进制格式序列化对象,同时也可以将对象序列化为许多不同的格式,它具有比 Java 标准序列化更高的性能。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞

mark 一下,另外,有和 Hessian 这个序列化框架的对比嘛

小狄子 回复

还没有。 hessian 我第一次听说,先研究研究。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册