一、定义以及相关概念
分布式系统的产生带了系统间通讯的需求,而互联通讯的双方需要采用约定的协议,
序列化和反序列化是约定协议的一部分。通讯协议往往采用分层模型,在OSI七层协议模型中表示层的主要功能就是序列化与反序列化。
• 序列化: 将数据结构或对象转换成二进制串的过程。
• 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
二、序列化协议特性
每种序列化协议都有优点和缺点,它们在设计之初有自己独特的应用场景。在系统设计的过程中,需要考虑序列化需求的方方面面,
综合对比各种序列化协议的特性,最终给出一个折衷的方案。
1) 通用性
技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低了。
流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。
2) 可调试性/可读性
序列化和反序列化的数据正确性和业务正确性的调试往往需要很长的时间,良好的调试机制会大大提高开发效率。序列化后的二进制串往往不具备人眼可读性,为了验证序列化结果的正确性,写入方不得同时撰写反序列化程序,或提供一个查询平台–这比较费时;另一方面,如果读取方未能成功实现反序列化,这将给问题查找带来了很大的挑战–难以定位是由于自身的反序列化程序的bug所导致还是由于写入方序列化后的错误数据所导致。对于跨公司间的调试,由于以下原因,问题会显得更严重。
1.支持不到位,跨公司调试在问题出现后可能得不到及时的支持,这大大延长了调试周期。
2.访问限制,调试阶段的查询平台未必对外公开,这增加了读取方的验证难度。
如果序列化后的数据人眼可读,这将大大提高调试效率, XML和JSON就具有人眼可读的优点。
3) 性能
1.空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。
2.时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。
4) 可扩展性/兼容性
移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。
三、序列化技术
1) Java序列化机制Serializable接口
1、实现方式
a) 实现Serializable接口
b) ObjectInputStream : 表示读取指定的字节数据转换成对象
c) ObjectOutputStream :将对象转为字节数据
2、serialVersionUID的作用
简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。
当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class(它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的)自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释,等等),就算再编译多次,serialVersionUID也不会变化的。
通过下面测试例子来具体看一下:
测试一:不设置serialVersionUID,序列化Person,不修改Person结构,反序列化Person
public class Person implements Serializable {
//private static final long serialVersionUID = 7670376133971818879L;
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
测试类:
public class Test {
/**
* 序列化Person
* @param person
* @throws IOException
*/
public static void serializePerson(Person person) throws IOException {
ObjectOutput out=new ObjectOutputStream(new FileOutputStream(new File("person")));
out.writeObject(person);
out.flush();
out.close();
}
/**
* 反序列化Person
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public static Person deSerializePerson() throws IOException, ClassNotFoundException {
ObjectInput in=new ObjectInputStream(new FileInputStream(new File("person")));
Person person=(Person)in.readObject();
in.close();
return person;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person=new Person();
person.setAge(19);
person.setName("zhangsan");
Test.serializePerson(person);//先序列化
Person person2=Test.deSerializePerson();//后反序列化
System.out.println(person2.toString());
}
}
运行结果:
Person{age=19, name=’zhangsan’}
因为在序列化以后我们并没有改变Person类的结构,所以jvm自动生成的serialVersionUID是一致的,所以反序列化是没有问题的。
测试二:不设置serialVersionUID,序列化Person,修改Person结构,反序列化Person
在Person结构没变的情况下,先序列化Person
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person=new Person();
person.setAge(19);
person.setName(“zhangsan”);
Test.serializePerson(person);//先序列化
//Person person2=Test.deSerializePerson();//后反序列化
//System.out.println(person2.toString());
}
修改Person结构,增加sex属性
public class Person implements Serializable {
//private static final long serialVersionUID = 7670376133971818879L;
private int age;
private String name;
private String sex;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
反序列化Person
public static void main(String[] args) throws IOException, ClassNotFoundException {
/* Person person=new Person();
person.setAge(19);
person.setName(“zhangsan”);
Test.serializePerson(person);//先序列化*/
Person person2=Test.deSerializePerson();//后反序列化
System.out.println(person2.toString());
}
运行结果:
Exception in thread “main” java.io.InvalidClassException: cn.creditease.javaSerialize.Person; local class incompatible: stream classdesc serialVersionUID = 858695653014971034, local class serialVersionUID = 2295568510989960539
测试三:设置serialVersionUID,序列化Person,修改Person结构,反序列化Person
public class Person implements Serializable {
private static final long serialVersionUID = 7670376133971818879L;
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
序列化Person
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person=new Person();
person.setAge(19);
person.setName(“zhangsan”);
Test.serializePerson(person);//先序列化
/*Person person2=Test.deSerializePerson();//后反序列化
System.out.println(person2.toString());*/
}
修改Person结构,新增sex属性
public class Person implements Serializable {
private static final long serialVersionUID = 7670376133971818879L;
private int age;
private String name;
private String sex;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
反序列化Person
public static void main(String[] args) throws IOException, ClassNotFoundException {
/* Person person=new Person();
person.setAge(19);
person.setName(“zhangsan”);
Test.serializePerson(person);//先序列化*/
Person person2=Test.deSerializePerson();//后反序列化
System.out.println(person2.toString());
}
运行结果:
Person{age=19, name=’zhangsan’, sex=’null’}
静态变量的序列化
序列化并不保存静态变量的状态
Transient关键字
transient关键字表示指定属性不参与序列化
父子类问题
如果父类没有实现序列化,而子类实现列序列化。那么父类中的成员没办法做序列化操作
序列化的存储规则
对同一个对象进行多次写入,打印出的第一次存储结果和第二次存储结果,只多了5个字节的引用关系。
并不会导致文件累加
序列化实现深度克隆
public class Person implements Serializable {
private static final long serialVersionUID = 7670376133971818879L;
private int age;
private String name;
private String sex;
private Person parent;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Person getParent() {
return parent;
}
public void setParent(Person parent) {
this.parent = parent;
}
/**
* 深拷贝
* @return
*/
public Person deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutput oo=new ObjectOutputStream(bos);
oo.writeObject(this);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInput oi=new ObjectInputStream(bis);
return (Person) oi.readObject();
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", parent=" + parent +
'}';
}
}
测试类:
public static void main(String[] args) throws IOException, ClassNotFoundException {
/* Person person=new Person();
person.setAge(19);
person.setName(“zhangsan”);
Test.serializePerson(person);//先序列化*/
/* Person person2=Test.deSerializePerson();//后反序列化
System.out.println(person2.toString());*/
Person parent=new Person();
parent.setName(“张三”);
Person person1=new Person();
person1.setName("张小三");
person1.setParent(parent);
//克隆张小三
Person person2=person1.deepClone();
person2.setName("李小四");
person2.getParent().setName("李四");
System.out.println(person1);
System.out.println(person2);
}
输出结果:
Person{age=0, name=’张小三’, sex=’null’, parent=Person{age=0, name=’张三’, sex=’null’, parent=null}}
Person{age=0, name=’李小四’, sex=’null’, parent=Person{age=0, name=’李四’, sex=’null’, parent=null}}
2) XML
优点:
1. 跨机器,跨语言
2. 可读性强
缺点:
1. 序列化之后的数据量剧增;
2. 序列化和反序列化的空间和时间开销都比较大。
对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。
3) JSON
优点:
1. 它保持了XML的人眼可读(Human-readable)的优点;
2. 相对于XML而言,序列化后的数据更加简洁;
3. 与XML相比,其协议比较简单,解析速度比较快。
4. 松散的Associative array使得其具有良好的可扩展性和兼容性。
缺点:
1.空间开销大
适用场景:
JSON在很多应用场景中可以替代XML,更简洁并且解析速度更快。典型应用场景包括:
1. 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
2. 基于Web browser的Ajax请求。
3. 由于JSON具有非常强的前后兼容性,对于接口经常发生变化,并对可调式性要求高的场景,例如Mobile app与服务端的通讯。
4. 由于JSON的典型应用场景是JSON+HTTP,适合跨防火墙访问。总的来说,采用JSON进行序列化的额外空间开销比较大,对于大数据量服务或持久化,这意味着巨大的内存和磁盘开销,这种场景不适合。实际操作中往往只能采用文档方式来进行约定,这可能会给调试带来一些不便,延长开发周期。 由于JSON在一些语言中的序列化和反序列化需要采用反射机制,所以在性能要求为ms级别,不建议使用。
JAVA平台开源组件: Google 的 Gson、
FasterXML 的 Jackson
阿里的fastjson
4) Protobuf(Google)
5) Kryo(只支持java)
性能与Protobuf相当,甚至更高
6) Avro(Apache)
Avro的产生解决了JSON的冗长和没有IDL的问题,Avro属于Apache Hadoop的一个子项目。 Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。 Avro支持的数据类型非常丰富,包括C#### 典型应用场景和非应用场景 Avro解析性能高并且序列化之后的数据非常简洁,比较适合于高性能的序列化服务。
由于Avro目前非JSON格式的IDL处于实验阶段,而JSON格式的IDL对于习惯于静态类型语言的工程师来说不直观。
7) Thrift
五、Benchmark以及选型建议
以下数据来自https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking。
选型建议:
1. 对于公司间的系统调用,如果性能要求在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。
2. 基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。
3. 对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。
4. 当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系。
5. 对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在Hadoop子项目里,Avro会是更好的选择。
6. 由于Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。
7. 对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。
8. 如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。
9. 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。