写点什么

「一探究竟」迷之序列化,Java 性能优化最佳实践

用户头像
极客good
关注
发布于: 刚刚
  • 服务端不可随意在任意位置增加字段,因为客户端不升级的话会导致反序列化失败

  • 不能使用第三方包提供的集合类工具包作为返回值


使用方式如下:


// 其中 StuDemo 类需要增加 @Message 注解标识需要被 MessagePack 序列化


// MessagePack 序列化方式不需要依赖 Serializable


public static void main(String[] args) throws IOException {


StuDemo demo = new StuDemo("Kerwin");


MessagePack pack = new MessagePack();


// 序列化


byte[] bytes = pack.write(demo);


// 反序列化


StuDemo res = pack.read(bytes, StuDemo.class);


System.out.println(res.getName());


}


复制代码


PS:我司的 RPC 框架目前就使用的 MessagePack 序列化方式,也是因为此,所以上述调整 serialVersionUID 时没有发生任何问题 同理,受制于底层序列化的限制,我们的新人文档中也明确提到了上述的限制,比如必须在最末尾增加字段等等。

Hessian2 序列化

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架,在 Hessian 的基础之上,Hessian2 的性能和压缩率大大提升。


Hessian 会把复杂的对象所有属性存储在一个类似 Map 的结构中进行序列化,所以在父类、子类中存在同名成员变量的情况下,它先序列化子类,然后序列化父类,因此会导致子类同名成员变量的值被父类覆盖等情况。


它有八大核心设计目标,官网


  • 必须自我描述序列化类型,即不需要外部模式或接口定义

  • 必须与语言无关,包括支持脚本语言

  • 必须在一次传递中可读或可写

  • 必须尽可能紧凑(压缩)

  • 必须简单

  • 必须尽可能快

  • 必须支持 Unicode 字符串

  • 必须支持 8 位二进制数据

  • 必须支持加密


使用方式如下:


public class StuHessianDemo implements Serializable {


private static final long serialVersionUID = -640696903073930546L;


private String name;


public StuHessianDemo(String name) {


this.name = name;


}


public String getName()


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


{


return name;


}


public void setName(String name) {


this.name = name;


}


}


复制代码


public static void main(String[] args) throws IOException {


StuHessianDemo hessianDemo = new StuHessianDemo("Kerwin");


ByteArrayOutputStream stream = new ByteArrayOutputStream();


HessianOutput hessianOutput = new HessianOutput(stream);


hessianOutput.writeObject(hessianDemo);


ByteArrayInputStream inputStream = new ByteArrayInputStream(stream.toByteArray());


// Hessian 的反序列化读取对象


HessianInput hessianInput = new HessianInput(inputStream);


System.out.println(((StuHessianDemo) hessianInput.readObject()).getName());


}


// 结果:Kerwin


复制代码

选择的依据

由上文我们得知了几种常用的序列化方式,及其优劣,比如 MessagePack 就是极致的压缩和快,Hessian2 则依赖 Serializable 接口,在保证安全性、自身描述性的基础上,尽可能的追求空间利用率,效率等,而 Java 序列化方式则一直被诟病,难等大雅之堂,因此在 RPC 框架选择底层序列化方式时,需要根据自身所需,有所侧重的选择某一项序列化方式。


选择的依据如下,优先级从高到低:



一点思考



JSON 序列化的地位

其实 JSON 序列化才是我们最熟知的序列化方式,它本身也不需要实现 Serializable 接口,为什么大多数 RPC 框架没有选择用它作为默认的序列化方式呢?


在了解完上文的内容后,我们知道关键还是在性能,效率、空间开销上,因为 JSON 是一种文本类型序列化框架,采用 KEY-VALUE 的方式存储数据,它在进行序列化的额外空间开销相对就更大,在反序列化时更不必说,需要依赖反射,因此性能进一步缩水。


然而 JSON 本身又具备极强的可读性、因此被作为 Web 中 HTTP 协议的事实标准。

为什么还要自定义 serialVersionUID

在《Effect Java》中有一句提到:


不管你选择了哪种序列化方式,都要为自己编写的每个可序列化的类声明一个显式的序列版本 UID。


为什么架构师会提醒我实现它?为什么书中也会这么说?


serialVersionUID 分解下来全称为:serial Version UID,序列版本 UID,每一个可序列化的类都有一个 long 域中显式地指定该编号,如果编码者未定义的话,系统就会对这个类的结构运用一个加密的散列函数(SHA-1),从而在运行时自动产生该标识号,该编号会受类名称、接口名称、公有及受保护的成员变量所影响,一旦有相关改动例如增加一个不重要的公有方法即会影响 UID,导致异常发生。


因此这是一个习惯问题,也是为了避免潜在风险。


总结


--


截止到这里,我们了解了原来之前学习到的 Java 序列化是那么的不实用(甚至到了被吐槽的地步),也知晓了一些框架使用注意事项底层的秘密(比如 MsgPack 增加字段),下面是关于序列化的一些小建议:


  1. 无论是否依赖Serializable,接口出参都建议实现序列化接口。

  2. 如果实现了序列化接口,务必自行实现 serialVersionUID。

  3. 接口出参对象不宜使用特殊的数据类型(如 MsgPack 第三方集合等)、过于复杂的结构(继承等),不然会导致很多莫名其妙的问题发生。

  4. 当发生服务端/客户端数据不一致时,第一时间想到是序列化问题,并针对当前序列化方式的特点,仔细排查。


如果觉得这篇内容对你有帮助的话:


  1. 当然要点赞支持一下啦~


参考资料




用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
「一探究竟」迷之序列化,Java性能优化最佳实践