面试官:看你简历说精通序列化与反序列化

2022-11-30 15:28:28 浏览数 (1)

很多大厂面试MQ问题,不会局限在使用,更多考察实现原理。

TCP连接传输数据的基本形式二进制流。一般编程语言或网络框架提供的API中,传输数据的基本形式是字节。二进制流和字节流本质上其实是一样的。

而对于我们编写的程序,需要通过网络传输的数据是结构化的数据形式:比如,一条命令、一段文本或者是一条消息,都可用类表示。

因此要想使用网络框架API传输结构化数据,必须实现结构化数据与字节流间的转换。

  • 结构化数据转换成字节流,称为序列化,反之就是反序列化。

序列化除了用于在网络上传输数据,另外一个重要用途是将结构化数据保存在文件,因为在文件内保存数据的形式也是二进制,和网络传输过程中的数据本质是一样的。

很多海量数据场景,都需将对象序列化后,把它们暂时从内存转移到磁盘,等要用时,再把数据从磁盘中读取出来,反序列化成对象来使用,这样不仅可以长期保存不丢失数据,而且可以节省有限的内存空间。

怎么来实现高性能的序列化和反序列化呢。

1 序列化技术选型

只是实现序列化和反序列功能,方法有很多,最常用的直接把一个对象转换成字符串打印,就是一种序列化实现,字符串只要转成字节序列,就可在网络上传输或保存在文件。但这种实现的方式仅是能用。

有很多通用序列化实现,可直接拿来用。Java和Go语言都内置了序列化实现,也有一些流行的开源序列化实现,比如,

  • Google 的Protobuf、Kryo、Hessian
  • 像JSON、XML这些标准的数据格式,也可以作为一种序列化实现
  • 也可以自己来实现私有的序列化实现。

如何选型?

  • 序列化后的数据最好是易于阅读
  • 实现的复杂度是否低
  • 序列化和反序列化的速度越快越好
  • 序列化后的信息密度越大越好,即同样的一个结构化数据,序列化之后占用的存储空间越小越好

当然,不会存在一种序列化实现在这四个方面都是最优的。因为易于阅读和信息密度总是矛盾的,实现复杂度和性能也是矛盾。还是根据业务需求选择合适的序列化实现。

  • JSON、XML这些序列化方法,可读性最好,但信息密度最低
  • Kryo、Hessian这些通用二进制序列化实现,适用范围广,使用简单,性能比JSON、XML要好,但不如专用的序列化实现

对强业务类系统,比如电商、社交类,特点是业务复杂,需求变化快,但对性能要求没那么苛刻。推荐使用JSON这种实现简单,数据可读性好的序列化实现,这种实现使用起来非常简单,序列化后的JSON数据我们都可以看得懂,无论是接口调试还是排查问题都非常方便。付出的代价就是多点CPU时间和存储空间。

比如序列化一个User对象,它包含3个属性,姓名zhangsan,年龄:23,婚姻状况:已婚。

代码语言:javascript复制
User:
  name: "zhangsan"
  age: 23
  married: true

使用JSON序列化后:

代码语言:javascript复制
{"name":"zhangsan","age":"23","married":"true"}

数据可直接看懂。

序列化的代码也较简单,直接调用JSON序列化框架提供的方法即可:

代码语言:javascript复制
byte [] serializedUser = 
JsonConvert.SerializeObject(user).getBytes("UTF-8");

若JSON序列化性能达不到业务要求,可采用性能更好的二进制序列化,实现的复杂度和JSON序列化差不多,都很简单,但序列化性能更好,信息密度也更高,只是失去可读性。

比如Kryo序列化User对象:

代码语言:javascript复制
kryo.register(User.class);
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeObject(output, user);

向Kryo注册一下User类,然后创建一个流,最后调用writeObject方法,将user对象序列化后直接写到流中。过程非常简单。

2 压榨序列化和反序列化的性能

绝大部分系统,使用上面这两类通用序列化实现都可满足需求,而像MQ这种解决通信问题的中间件,对性能要求非常高,通用序列化实现达不到性能要求,所以,很多MQ自己实现专用序列化和反序列化。

使用专用的序列化方法,可提高序列化性能,并有效减小序列化后的字节长度。

在专用序列化方法中,不必考虑通用性。比如,可固定字段顺序,这样在序列化后的字节里面就不必包含字段名,只要字段值即可,不同类型数据也可做针对性优化:

对于同样的User对象,我们可以把它序列化成:

代码语言:javascript复制
03   | 08 7a 68 61 6e 67 73 61 6e | 17 | 01
User |    z  h  a  n  g  s  a  n  | 23 | true

这神奇的序列化方法是怎么表示User对象的呢。

首先需要标识对象类型,这里用个字节来表示类型,比如03表示User类型对象。 约定按name、age、married固定顺序序列化这三属性。 按顺序,第一个字段name,不存字段名,直接存字段值“zhangsan”即可,由于名字长度不固定,第一个字节08表该名字长度8个字节,紧随其后的8字节即zhangsan。

第二个字段年龄,一个字节即可,23的16进制17 。 最后的字段状态,一个字节:01已婚,00未婚。

可以看到,同样的一个User对象,JSON序列化后需要47个字节,这里只要12个字节就够了。

专用序列化方法更高效,序列化出字节更少,在网络传输过程速度更快。 但缺点是要为每种对象类型定义专门的序列化和反序列化方法,实现起来复杂,大部分情况下都不划算。

3 总结

进程之间要通过网络传输结构化的数据,需通过序列化反序列化实现结构化数据和二进制数据转换。选型要综合考虑数据可读性,实现复杂度,性能和信息密度这几点。

大多数情况下,选择一个高性能的通用序列化框架都可以满足要求,在性能可以满足需求的前提下,推荐优先选择JSON这可读性好的序列化方法。

如果说需要超高性能或带宽有限,可使用专用序列化方法,提升序列化性能,节省传输流量。不过实现复杂,大部分情况下并不划算。

4 面试场景快问快答

在内存里存放的任何数据,最基础的存储单元也是二进制,即应用程序操作的对象,在内存中也是二进制存储的,既都是二进制,为什不直接把内存中对象对应的二进制数据直接通过网络发出去或保存在文件?为什么还需要序列化和反序列化呢? 内存中的对象数据应该具有语言独特性,例如表达相同业务的User对象(id/name/age字段),Java和PHP在内存中的数据格式应该不一样的,如果直接用内存中的数据,可能会造成语言不通。 通常两个服务之间没有严格要求语言必须一致,只要对序列化的数据格式进行了协商,任何2个语言直接都可以进行序列化传输、接收。

虽然都是二进制的数据,但是序列化的二进制数据是通过一定的协议将数据字段进行拼接。第一个优势是:不同的语言都可以遵循这种协议进行解析,实现了跨语言。第二个优势是:这种数据可以直接持久化到磁盘,从磁盘读取后也可以通过这个协议解析出来。如果是内存中的数据不能直接存盘的,直接存盘后再读出来我们根本无法辨识这是个什么数据。

本地应用通过基地址 偏移量的方式访问内存中的成员变量,因为它知道偏移量代表什么。通过网络发给对方的话,对方并不知道偏移量代表什么,所以无法解析。 举一个例子,如果直接取LinkedList在内中的字节流作为序列化结果,那么只能包含一个头节点和一个子节点,必须使用LinkedList自己实现的writeObject方法进行序列化。

面临问题:

  1. 网络字节序与主机字节序问题,业务要感知和处理大小端问题
  2. 平台差异,各平台对基本数据类型的长度定义不一致、结构体对齐策略不一致,不同os有大小端存储之分,无法实现平台兼容
  3. 连续内存问题,一个对象可能引用,指向其他对象,指针就是一个地址,传输后在另外的设备上是无效值。数据在内存中大多以地址链接的离散数据为主,且因为内存里的对象不是一串连续的字节流,而是通过地址相互引用,比如map,其值是一个地址,表示值在哪里,而不是值本身。

如果解决这些问题了,也变相实现了自己的序列化框架。

  • 一个c/s的架构应用,需要实现client之间的点对点数据通信以及群组通信 实际上就是一个即时通讯应用 由于没有即时通讯相关的经验 还请老师能够指导一下。其中的数据传输使用MQ转发 还是基于netty自定义?自己两种方式都琢磨了一下,基于MQ的话,topic tag会很多 只要涉及一端client 操作(比如:打开某个界面)需要同步到其他client的话 就需要对topic进行生产以及订阅消费。 第二就是基于netty自定义,这种情况下c端和s端都要定义一个类似servlet或者springmvc里面的dispatcher根据相关参数分发到具体的业务方法 一般来说,即时通信类系统并不适合用消息队列来实现。很多即时通讯软件都是使用一些P2P技术,数据直接点对点传输,不经过服务端转发的。

0 人点赞