服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - 编程技术 - 用数据说话,序列化框架测评报告

用数据说话,序列化框架测评报告

2022-03-02 22:13爱笑的架构师雷小帅 编程技术

今天选择几款市面上常用的序列化框架进行测试对比,帮助开发团队搞清楚不同场景该采用哪种序列化框架。

序列化是我们在日常开发中经常会使用到的技术,比如需要将内存对象持久化存储、需要将对象通过网络传输到远端。目前市面上序列化框架非常多,开发团队在进行技术选型时通常难以抉择,甚至会踩坑。

今天选择几款市面上常用的序列化框架进行测试对比,帮助开发团队搞清楚不同场景该采用哪种序列化框架。

测试对比的框架有四款:

JDK原生、fastjson、Kryo、Protobuf

接下来会从以下这四个方面给出详细的测试对比结果:

(1)是否通用:是否支持跨语言、跨平台;

(2)是否容易使用:是否编译使用和调试;

(3)性能好不好:序列化性能主要包括时间开销和空间开销,时间开销是指序列化和反序列化对象所耗费的时间,空间开销是指序列化生成数据大小;

(4)可扩展强不强:随着业务发展,传输的业务对象可能会发生变化,比如说新增字段,这个时候就要看所选用的序列化框架是否有良好的扩展性;

框架1:JDK原生

是否通用?

JDK 原生是 Java 自带的序列化框架,与 Java 语言是强绑定的,通过 JDK 将对象序列化后是无法通过其他语言进行返序列化的,所以它的通用性比较差。

是否容易使用?

一个类实现了java.io.Serializable序列化接口就代表这个类的对象可以被序列化,否则就会报错。

简单认识一下Serializable这个类,通过看源码我们知道Serializable仅仅是一个空接口,没有定义任何方法。

public interface Serializable { } 

这说明Serializable仅仅是一个标识的作用,用来告诉 JVM 这个对象可以被序列化。

想真正完成对象序列化和反序列化还得借助 IO 核心操作类:ObjectOutputStream和ObjectInputStream。

  /**  * 序列化  *  * @param obj 待序列化对象  * @return 二进制字节数组  * @throws IOException  */ public static byte[] serialize(Object obj) throws IOException { // 字节输出流
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 将对象序列化为二进制字节流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); // 获取二进制字节数组
        byte[] bytes = byteArrayOutputStream.toByteArray(); // 关闭流
        objectOutputStream.close(); byteArrayOutputStream.close(); return bytes; } 

ObjectInputStream类的readObject()方法用于从 IO 流中读取对象,完成对象反序列化:

  /**  * 反序列化  *  * @param bytes 待反序列化二进制字节数组  * @param  反序列对象类型  * @return 反序列对象  * @throws IOException  * @throws ClassNotFoundException  */ public static <T> T deSerialize(byte[] bytes) throws IOException, ClassNotFoundException { // 字节输入流
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); // 将二进制字节流反序列化为对象
        final ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); final T object = (T) objectInputStream.readObject(); // 关闭流
        objectInputStream.close(); byteArrayInputStream.close(); return object; } 

从上面的代码可以看出,JDK 原生框架使用起来还是有点麻烦的,首先要求对象必须实现java.io.Serializable接口,其次需要借助 IO 流操作来完成序列化和反序列化。与市面上其他开源框架比起来,上面的代码写起来非常生硬。

一句话总结:JDK 原生框架易用性稍差。

性能好不好?

(1)序列化体积测试

为了方便测试对比,我定义了一个普通 java 类,后面其他框架的测试基本上也是用这个类:

public class UserDTO implements Serializable { private String name; private String wechatPub; private String job; …… } 

将 UserDTO 类进行实例化

UserDTO userDTO = new UserDTO(); userDTO.setName("雷小帅"); userDTO.setWechatPub("微信公众号:爱笑的架构师"); userDTO.setJob("优秀码农"); 

序列化和反序列化测试:

System.out.println("--- 1. jdk 原生测试 ---"); byte[] bytes = JDKSerializationUtil.serialize(userDTO); System.out.println("序列化成功:" + Arrays.toString(bytes)); System.out.println("byte size=" + bytes.length); UserDTO userDTO1 = JDKSerializationUtil.deSerialize(bytes); System.out.println("反序列化成功:" + userDTO1); 

打印出来的结果:

--- 1. jdk 原生测试 --- 序列化成功:[-84, -19, 0, 5, 115, 114, 0, 39, ……
byte size=182 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农'] 

一个 UserDTO 序列化完之后是 182 个字节,待会对比其他框架就知道,这个水平太差了,Java 原生是自带的序列化工具,亲儿子也不给力啊。

(2)序列化速度测试

接下来我们再测试一下序列化和反序列化的速度,总共循环 100 万次:

  • JDK 序列化耗时:2314 毫秒
  • JDK 反序列化耗时:4170 毫秒

这个成绩怎么样,后面揭晓。

可扩展强不强?

JDK 原生序列化工具通过在类中定义 serialVersionUID 常量来控制版本:

private static final long serialVersionUID = 7982581299541067770L; 

上面这个serialVersionUID是通过 IDEA 工具自动生成的长整形。其实你也可以不用声明这个值,JDK 会根据 hash 算法自动生成一个。

如果序列化的时候版本号是当前这个值,反序列化前你将值改变了,那么反序列化的时候就会报错,提示 ID 不一致。

假如需要在 UserDTO 这个类再加一个字段,那如何支持扩展呢?

你可以改变一下serialVersionUID值就可以了。

框架2:fastjson

是否通用?

fastjson 是阿里巴巴出品的一款序列化框架,可以将对象序列化为 JSON 字符串,类似的框架还有 jackson, gson 等。

由于 JSON 是与语言和平台无关,因此它的通用性还是很好的。

是否容易使用?

UserDTO 类不需要实现 Serializable 接口,也不需要加 serialVersionUID 版本号,使用起来非常简单。

将一个对象序列化为 json 字符串:

com.alibaba.fastjson.JSON.toJSONString(obj); 

将 json 字符串反序列化为指定类型:

com.alibaba.fastjson.JSON.parseObject(jsonString, clazz); 

另外 fastjson 框架还提供了很多注解,可以在 UserDTO 类进行配置,实现一些定制化的功能需求。

性能好不好?

(1)序列化体积测试

跟 JDK 原生框架一样,假设我们已经实例化好了一个UserDTO 对象,分别进行序列化和反序列化测试:

System.out.println("--- 2. fastjson 测试 ---"); String jsonString = FastjsonSerializationUtil.serialize(userDTO); System.out.println("序列化成功: " + jsonString); System.out.println("byte size=" + jsonString.length()); UserDTO userDTO2 = FastjsonSerializationUtil.deSerialize(jsonString, UserDTO.class); System.out.println("反序列化成功:" + userDTO2); 

上面的代码是将序列化和反序列化代码封装到了一个工具类中。运行输出结果:

--- 2. fastjson 测试 --- 序列化成功: {"job":"优秀码农","name":"雷小帅","wechatPub":"微信公众号:爱笑的架构师"} byte size=54 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农'] 

可以看到序列化之后有 54 个字节,而上面 JDK 原生框架是182 个字节,对比下来发现 fastjson 确实比 JDK 原生框架强了不少,亲儿子真不行。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • fastjson 序列化耗时:287 毫秒
  • fastjson 反序列化耗时:365 毫秒

这个结果简直,人如其名啊,真快~ 你看看隔壁 JDK 原生框架的速度,惨不忍睹,哎……

可扩展强不强?

fastjson 没有版本控制机制,如果对类进行修改,比如新增熟悉字段,反序列时可以进行配置,忽略不认识的熟悉字段就可以正常进行反序列化。

所以说 fastjson 的扩展性还是很灵活的。

框架3:Kryo

是否通用?

Kryo 是一个快速高效的二进制序列化框架,号称是 Java 领域最快的。它的特点是序列化速度快、体积小、接口易使用。

Kryo支持自动深/浅拷贝,它是直接通过对象->对象的深度拷贝,而不是对象->字节->对象的过程。

关于 Kryo 更多的介绍可以去 Github 查看:

https://github.com/EsotericSoftware/kryo

关于通用性,Kryo 是一款针对 Java 语言开发的框架,基本很难跨语言使用,因此通用性比较差。

是否容易使用?

先引入 Kryo 依赖:

<dependency> <groupId>com.esotericsoftwaregroupId> <artifactId>kryoartifactId> <version>5.3.0version> dependency> 

Kryo 提供的 API 非常简洁,Output 类封装了输出流操作,使用 writeObject 方法将对象写入 output 输出流程即可完成二进制序列化过程。

下面代码封装了一个简单的工具方法:

/**  * 序列化  *  * @param obj  待序列化对象  * @param kryo kryo 对象  * @return 字节数组  */ public static byte[] serialize(Object obj, Kryo kryo) { Output output = new Output(1024); kryo.writeObject(output, obj); output.flush(); return output.toBytes(); } 

Kryo 反序列化也非常简单,Input 封装了输入流操作,通过 readObject 方法从输入流读取二进制反序列化成对象。

/**  * 反序列化  *  * @param bytes 待反序列化二进制字节数组  * @param    反序列对象类型  * @return 反序列对象  */ public static <T> T deSerialize(byte[] bytes, Class<T> clazz, Kryo kryo) { Input input = new Input(bytes); return kryo.readObject(input, clazz); } 

另外 Kryo 提供了丰富的配置项,可以在创建 Kryo 对象时进行配置。

总体而言,Kryo 使用起来还是非常简单的,接口易用性也是非常不错的。

性能好不好?

(1)序列化体积测试

Kryo 框架与其他框架不同,在实例化的时候可以选择提前注册类,这样序列化反序列化的速度会更快,当然也可以选择不注册。

System.out.println("--- 3. kryo 测试 ---"); Kryo kryo = new Kryo(); kryo.setRegistrationRequired(false); // kryo.register(UserDTO.class); byte[] kryoBytes = KryoSerializationUtil.serialize(userDTO, kryo); System.out.println("序列化成功:" + Arrays.toString(kryoBytes)); System.out.println("byte size=" + kryoBytes.length); UserDTO userDTO3 = KryoSerializationUtil.deSerialize(kryoBytes, UserDTO.class, kryo); System.out.println("反序列化成功:" + userDTO3); 

运行结果:

序列化成功:[-123, -28, -68, -104, -25, ……] byte size=60 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农'] 

从结果来看,序列化后总共是 60 字节。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • kryo 序列化耗时:295 毫秒
  • kryo 反序列化耗时:211 毫秒

这个成绩还不错。

可扩展强不强?

Kryo默认序列化器 FiledSerializer 是不支持字段扩展的,如果想要使用扩展序列化器则需要配置其它默认序列化器。

框架4:Protobuf

是否通用?

Protobuf 是谷歌开源的一款二进制序列化框架。

Protobuf 要求先写schema描述文件,然后通过编译器编译成具体的编程语言(Java、C++、Go 等),因此它是一种语言中立、跨平台的框架,通用性非常好。

是否容易使用?

先编写 schema 文件,定义了一个 User 类,拥有三个属性字段:

syntax = "proto3"; option java_package = "com.example.demo2.serialization.protobuf"; message User { string name = 1; string wechatPub = 2; string job = 3; } 

接着在电脑上安装好 Protobuf 编译工具,执行编译命令:

protoc --java_out=./  user-message.proto 

编译成功后会生成一个 UserMessage 类。

UserMessage 类包含了很多内容:

首先有一个 Builder 内部类,可以用于实例化对象;

另外还提供了toByteArray(),可以很方便将对象序列化为二进制字节数组;提供了parseFrom()方法可以将对象反序列化为对象。

在接口使用上非常简单,开箱即用。

性能好不好?

(1)序列化体积测试

使用上面生成的UserMessage类创建一个对象,然后再进行序列化和反序列化测试:

System.out.println("--- 4. protobuf 测试 ---"); UserMessage.User user = UserMessage.User.newBuilder() .setName("雷小帅") .setWechatPub("微信公众号:爱笑的架构师") .setJob("优秀码农") .build(); final byte[] protoBufBytes = user.toByteArray(); System.out.println("序列化成功:" + Arrays.toString(protoBufBytes)); System.out.println("byte size=" + protoBufBytes.length); final UserMessage.User user1 = UserMessage.User.parseFrom(protoBufBytes); System.out.println("反序列化成功:" + user1); 

运行结果:

序列化成功:[-123, -28, -68, -104, -25, ……] byte size=63 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农'] 

序列化后是 63 字节,比 Kryo 稍微多一点点,有点吃惊。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • protobuf 序列化耗时:93 毫秒
  • protobuf 反序列化耗时:341 毫秒

序列化速度很强,但是反序列化为什么慢这么多?

可扩展强不强?

可扩展性是 Protobuf 设计目标之一,我们可以很方便进行字段增删,新旧协议都可以进行解析。

总结:

本文对常用的框架进行了测试对比,通过观察 是否通用、是否容易使用、性能好不好、可扩展强不强 这四种维度,我们发现它们各有优劣,大家在进行技术选型时一定要慎重。

最后针对性能测试这一块,简单总结一下,给每种框架排个序。

(1)序列化体积

fastjson 54 bytes < Kryo 60 bytes < Protobuf 63 bytes < Java 原生 182 bytes

体积越小,传输效率越高,性能更优。Java 亲儿子真惨!

(2)序列化速度

protobuf 93 毫秒 < fastjson 289 毫秒 < kryo 295 毫秒 < Java 原生 2247 毫秒

Protobuf 真牛逼,王者!Java 亲儿子继续输~

(3)反序列化速度

kryo 211 毫秒 < protobuf 341 毫秒 < fastjson 396 毫秒 < Java 原生 4061 毫秒

Kryo 成绩比较稳定,序列化和反序列用时接近。Java 亲儿子输麻了!

原文地址:https://mp.weixin.qq.com/s/Z9ahavCHPxQRfBzjuVJwSg

延伸 · 阅读

精彩推荐