序列化实际场景对比

序列化

具体使用请参考: 序列化方案使用示例
序列化的过程就是将对象转变成字节码,反序列化即是从字节码转换成对象的过程一般情况下要求实现 Serializable 接口,此接口中没有定义任何成员,只是起到标记对象是否可以被序列化的作用。为何需要有序列化呢?一方面是为了存储在磁盘中,另一个作用就是作为网络远程传输的内容。

需要考虑的问题

序列化框架需要考虑的问题(通用问题)

  1. 对象序列化后的大小

    一个对象会被序列化工具序列化为一串byte数组,这其中包含了对象的field值以及元数据信息,使其可以被反序列化回一个对象

  2. 序列化与反序列化的速度

    一个对象被序列化成byte数组的时间取决于它生成/解析byte数组的方法

  3. 序列化工具本身的速度

    序列化工具本身创建会有一定的消耗

Java使用序列化框架需要考虑的问题(特殊问题)

  • 是否支持循环引用
  • 无默认构造函数
  • 反序列化后属性丢失程度(如对象的属性是一个接口的实现,反序列后是否丢失)
  • 被序列化的对象及其属性是否必须实现 Serializable 接口
  • 是否支持大对象序列化
  • 被序列化的对象中List、Map属性是否丢失,且对List、Map序列化效率及正确率
  • 是否嵌套或循环引用或内部类造成在序列化过程中 StackOverflow

实际开发遇到的问题

背景:Activiti6 缓存的类有了些许改变,变成了 ProcessDefinitionCacheEntry ,而如果要对这个类的实例对象做缓存,就要对其进行序列化,当序列化的时候却出现各种各样的问题

ProcessDefinitionCacheEntry 内部结构

public class ProcessDefinitionCacheEntry implements Serializable {
    protected ProcessDefinition processDefinition;
    protected BpmnModel bpmnModel;
    protected Process process;

    public ProcessDefinitionCacheEntry(ProcessDefinition processDefinition, BpmnModel bpmnModel, Process process) {
        this.processDefinition = processDefinition;
        this.bpmnModel = bpmnModel;
        this.process = process;
    }
}

分析:整体来说 ProcessDefinitionCacheEntry 内部结构非常简单,一共就 ProcessDefinition(接口)、BpmnModel、Process 三个属性

出现的问题

  • ProcessDefinition 为接口类型,实际的 ProcessDefinitionCacheEntry 实例中的 ProcessDefinition 为接口的实现类的实例对象,序列化和反序列化时是否会丢失或被擦除部分内容
  • BpmnModel 没有实现 Serializable 接口,序列化框架对实现 Serializable 接口的类的实例对象是否支持(特殊说明:实时上,不仅 Bpmn 本身没有实现 Serializable 接口,其内部的属性也没有实现,如 GraphicInfo)
  • BpmnModel 和 Process 中存在大量的 List 属性和Map 类型的属性,序列化框架是否对这些属性能良好的序列化(包括但不限于序列化速度、大小、数据丢失度、序列化时是否造成栈溢出、是否支持循环引用、序列化循环引用的属性时会不会造成死循环)
  • 序列化 ProcessDefinitionCacheEntry 的实例对象时,被序列化成 null 或属性被变成了 null,导致的数据不一致

同时也有人遇到了同样的问题,详细请看 http://www.blackzs.com/archives/1769 这篇发布于 20201206 的文章

解决方案过程

以下序列化方式是工作中真实的处理过程,并不是一步到位,经历了很多令人窒息的错误,但也收获了很多,理解了序列化框架的场景和缺陷

  1. 采用Json序列化方式:Jackson/Fastjson/Gson等,可以得到Json字符串,但由于 ProcessDefinitionCacheEntry 中有接口 (ProcessDefinition) 类型的属性,导致反序列化时该数据丢失。同时,作为数据传输的话,Json 会导致数据的类型(成员变量及自身所属的Class)丢失,需要反序列为原来的类型。json在反序列化时会丢失部分数据信息,如 接口的实现类型丢失、成员变量的类型丢失、数据的值丢失

    总结:使用Json序列化方式不可取(Json在正常的有 Getter/Setter 方法,没有Map、对象、接口等复杂类型的属性的情况下比较友好)

  2. 采用JDK序列化方式:JDK序列化方式在序列化的前后都能完整的保持对象的属性,包括接口的实现对象属性、Map、List等都能完整的保留,但是JDK序列化有个“致命”的弊端:被序列化的对象及其内部属性必须实现 Serializable 接口

    总结:使用JDK序列化方式不可取(JDK序列化能够完整的保留对象的各项信息,但要求对象类 的内部属性必须都实现 Serializable 接口,对于一些不可控的因素无法使用,如引用的 jar 内部类、没有权限改动的类、已经封版的类等)

  3. 采用Protostuff序列化方式:Protostuff是一种高性能、体积小的序列化方案。是google在原来的protobuffer是的优化产品。使用起来也比较简单易用,目前效率也是最好的一种序列化工具。 但是对于一些特殊的类型,Protostuff在序列化过程中可能造成严重的问题,如(实际序列化 ProcessDefinitionCacheEntry 时就发生了下述问题)

    • 1、每个类需要有默认的构造方法(即无参构造函数)
    • 2、嵌套对象时,子对象不可以是内部类,不然序列化时会栈溢出
    • 3、对于有大量List/Map的属性(且泛型可能为复杂类型,如List),序列化时可能会造成栈溢出,且序列化效率不高或可能非常差
    • 4、反序列化后接口类型的属性不完整(类型丢失或成员属性丢失,比如序列化前属性User接口的实际类型为UserImpl,反序列化后该类型就丢失了,因为Protostuff在序列化时不会保留UserImpl的类型)

    总结:使用Protostuff序列化方式不可取(Protostuff序列化要求必须有无参构造、对于List/Map 效率不高、类型可能丢失)

  4. 采用Hessian序列化方式:默认支持跨语言、序列化速度较Protostuff慢、序列化后的字节量和耗时稍高但尚可接受、可兼容数据类中字段的增删(Dubbo默认采用的序列化方式),但是有以下缺陷:

    • 1、每个类需要有默认的构造方法(非必须,但没有可能造成出错)
    • 2、Java数据类需实现 Serializable 接口(实际体验:当使用 Dubbo 框架时,所传输的参数对象没有实现 Serializable 接口就会报错)
    • 3、反序列化后接口类型的属性不完整(如接口属性)

    总结:使用Hessian序列化方式不可取(要求必须有无参构造、实现 Serializable 接口、接口的实际类型可能丢失【跨语言的必然】 )

  5. 采用Kryo序列化方式:是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的体积,且无需实现 Serializable 接口,且可将类型中的接口属性完整的反序列化回来,但目前只有java版本,不支持跨语言

    总结:使用Kryo序列化方式可取(对于复杂对象,包括含有接口成员属性的对象,都能够完整的反序列化,并且保留接口的具体实现类型),被序列化的对象无需实现 Serializable 接口

最终解决方案说明

采用了Kryo作为序列化方案,将 ProcessDefinitionCacheEntry 对象的以 byte 数组的方式保存在缓存中,且能够从缓存中取出且较为完整的反序列化回来,对于不需要跨语言的场景,可以考虑作为一个合适的序列化框架。可将数据序列化为 byte 数组后进行缓存、远程传输(RPC)、持久化到磁盘等操作

序列化框架对比

性能对比

序列化无默认构造函数循环引用对象为空是否需要预先知道对象所属的类大对象(4M)
Kryo支持需将reference选项打开支持不需要,关闭register支持
Java支持支持支持不需要支持
Protostuff支持支持支持不需要支持
Protostuff-runtime不支持支持支持需要支持
Hessian支持支持支持不需要支持

优缺点比较

 优点缺点
Kryo速度快,序列化后体积小跨语言支持较复杂
Java使用方便,可序列化所有类速度慢,占空间
Protostuff速度快,基于protobuf需静态编译
Protostuff-Runtime无需静态编译,但序列化前需预先传入schema不支持无默认构造函数的类,反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值
Hessian默认支持跨语言较慢

fst

与kryo类似是apache组织的一个开源项目,完全兼容JDK序列化协议的系列化框架,序列化速度大概是JDK的4-10倍,大小是JDK大小的1/3左右

官方文档: https://github.com/RuedigerMoeller/fast-serialization/wiki/Serialization

  • pom:
<dependency> 
    <groupId>de.ruedigermoeller</groupId> 
    <artifactId>fst</artifactId> 
    <version>2.56</version> 
</dependency>

部分序列化框架原理简单说明

具体参看:https://www.cnblogs.com/liouwei4083/p/6123383.html

Java序列化

原理:类需要实现 Serializable接口,才能被jdk自己的序列化机制序列化,jdk序列化的时候,会将这个类和他的所有超类都元数据,类描述,属性,属性值等等信息都序列化出来,这样就导致序列化后的大小比较大,速度也会比较慢,但是包含的内容最全面。可以完全反序列化

Kryo序列化

原理:序列化的时候,会将对象的信息,对象属性值的信息等进行序列化,而且没有将类field的描述信息进行序列化,这样就比jdk自己的序列化出来的小多了,而且速度肯定更快,但是包含的信息没有jdk的全面

Hessian序列化

原理:序列化的时候,也是将对象的信息,属性值信息等进行序列化,也会比jdk自己的序列化后的小很多,但是没有kryo的小,速度也挺快

Kryo和Hessian对比

  1. Kryo序列化后比Hessian小很多。(kryo优于hessian)
  2. 由于Kryo没有将类field的描述信息序列化,所以Kryo需要以自己加载该类的filed。这意味着如果该类没有在kryo中注册,或者该类是第一次被kryo序列化时,kryo需要时间去加载该类(hessian优于kryo)
  3. 由于2的原因,如果该类已经被kryo加载过,那么kryo保存了其类的信息,就可以很快的将byte数组填入到类的field中,而hessian则需要解析序列化后的byte数组中的field信息,对于序列化过的类,kryo优于hessian。
  4. hessian使用了固定长度存储int和long,而kryo则使用的变长,实际中,很大的数据不会经常出现。(kryo优于hessian)
  5. hessian将序列化的字段长度写入来确定一段field的结束,而kryo对于String将其最后一位byte+x70用于标识结束(kryo优于hessian)

扩展

他人心记

在学习dubbo的时候看到,dubbo默认的序列化方式是 hessian2序列化( hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的hessian lite,它是dubbo RPC默认启用的序列化方式),而再看dubbox的时候看到dubbox 引入Kryo和FST这两种高效Java序列化实现,来逐步取代hessian2。( 其中,Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。而FST是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例,但它还是非常有前途的。 在面向生产环境的应用中,我建议目前更优先选择Kryo。)下图简单说明了各种序列化框架序列化同一个对象后的字节大小比较

序列化实现请求字节数响应字节数
Kryo27290
FST28896
Dubbo Serialization430186
Hessian546329
FastJson461218
Json657409
Java Seialization963630

序列化参考文章:

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰式的美式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值