在分布式架构中,序列化是分布式的基础构成之一,咱们须要把单台设备上的数据经过序列化(编码、压缩)后经过网络传输给网络中的其它设备,从而实现信息交换。 JDK对Java中的对象序列化提供了支持,原生的Java序列化要求序列化的类必须实现java.io.Serializable
接口,该接口是一个标记接口(不包含任何方法)。 下面定义一个POJO类(仅用于演示,没有任何实际意义),它将被序列化和反序列化java
public class Data implements Serializable { private Integer a; private Long b; private Float c; private Double d; private Boolean e; private Character f; private Byte g; private Short h; private int a0; private long b0; private float c0; private double d0; private boolean e0; private char f0; private byte g0; private short h0; private String i; private Date j; // getter / setter ... }
使用Java序列化代码很是简单,咱们须要构造一个ObjectOutputStream
,该类接收一个输出流(用于输出序列化后的对象信息),这里为了方便演示,我用了ByteArrayOutputStream
,将对象序列为一个字节数组git
// 执行序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream output = new ObjectOutputStream(baos); output.writeObject(data); baos.close(); output.close(); byte[] buf = baos.toByteArray(); assertEquals(947, buf.length);
代码里省略了构造测试对象的代码(属性有点多),演示了序列化的过程,除了构造输出流和关闭注流代码,实际序列化代码只有一句:output.writeObject(data);
,因此Java的序列化代码实现仍是比较简单的。 测试代码中包含一个关于序列化后数据大小的测试,有947个字节,后面其它的序列化会与之造成对比。 当网络一端接收到这个字节数组(数据流)后,会执行反序列化,获得序列化前的数据,下面实现反序列化github
// 执行反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(buf); ObjectInputStream input = new ObjectInputStream(bais); Data data2 = (Data) input.readObject(); bais.close(); input.close(); assertFalse(data == data2); assertEquals(data.getA(), data2.getA()); assertEquals(data.getI(), data2.getI()); assertEquals(data.getJ(), data2.getJ());
代码里实现了将字节数组反序列化为一个Data对象,测试语句证实了反序列化对象与原对象不是一个对象(以前讲对象克隆时提到过可使用序列化、反序列化来实现,这里证实了这一点),但其属性都是一致的,也就是说咱们正确获得了序列化前的数据。apache
使用Serializable
实现序列化时,若是某一个或某几个字段不须要序列化,可使用transient
关键字修改字段便可json
private transient String password;
JDK还提供另外一种序列化方式,经过Externalizable
接口来实现数组
public class Data3 implements Externalizable { private Integer id; private String name; private Date birthday; @Override public void writeExternal(ObjectOutput output) throws IOException { output.writeInt(this.id); output.writeUTF(this.name); output.writeObject(this.birthday); } @Override public void readExternal(ObjectInput input) throws IOException, ClassNotFoundException { this.id = input.readInt(); this.name = input.readUTF(); this.birthday = (Date) input.readObject(); } // getter / setter ... }
这里不解释,其与Hadoop提供的序列化机制几乎相同,因此请参考Hadoop的序列化。网络
在Hadoop中因为常常须要向DataNode复制数据,Hadoop设计了一套特殊的序列化代码(实际还是彻底由JDK实现,其实现方式与Externalizable机制基本相似)。架构
public class Data2 { private Integer a; private Long b; private Float c; private Double d; private Boolean e; private Character f; private Byte g; private Short h; private int a0; private long b0; private float c0; private double d0; private boolean e0; private char f0; private byte g0; private short h0; private String i; private Date j; public byte[] serialize() throws IOException { return Data2.serialize(this); } /** * 序列化当前对象 * * @return */ public static final byte[] serialize(Data2 data) throws IOException { assert data != null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutput output = new DataOutputStream(baos); // 序列化的数据参考 JdkSerializeTest 中的Data对象 // 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强不少 output.writeInt(data.getA()); output.writeInt(data.getA0()); output.writeLong(data.getB()); output.writeLong(data.getB0()); output.writeFloat(data.getC()); output.writeFloat(data.getC0()); output.writeDouble(data.getD()); output.writeDouble(data.getD0()); output.writeBoolean(data.getE()); output.writeBoolean(data.isE0()); output.writeChar(data.getF()); output.writeChar(data.getF0()); output.writeByte(data.getG()); output.writeByte(data.getG0()); output.writeShort(data.getH()); output.writeShort(data.getH0()); writeString(output, data.getI()); // 序列化日期时使用时间戳表示 output.writeLong(data.getJ().getTime()); return baos.toByteArray(); } /** * 反序列化 Data2 对象 * * @param buf * @return */ public static final Data2 deserialize(byte[] buf) throws IOException { // 执行反序列化,注意读取的顺序与写入的顺序要一致 ByteArrayInputStream bais = new ByteArrayInputStream(buf); DataInput input = new DataInputStream(bais); Data2 data = new Data2(); data.setA(input.readInt()); data.setA0(input.readInt()); data.setB(input.readLong()); data.setB0(input.readLong()); data.setC(input.readFloat()); data.setC0(input.readFloat()); data.setD(input.readDouble()); data.setD0(input.readDouble()); data.setE(input.readBoolean()); data.setE0(input.readBoolean()); data.setF(input.readChar()); data.setF0(input.readChar()); data.setG(input.readByte()); data.setG0(input.readByte()); data.setH(input.readShort()); data.setH0(input.readShort()); data.setI(readString(input)); data.setJ(new Date(input.readLong())); return data; } /** * 向 DataOutput 写入字符类型稍微复杂一些 * * @param out * @param s * @throws IOException * @see org.apache.hadoop.io.WritableUtils#writeString(DataOutput, String) */ private static final void writeString(DataOutput out, String s) throws IOException { if (s != null) { byte[] buffer = s.getBytes("UTF-8"); int len = buffer.length; // 先写入字符串长度 out.writeInt(len); // 再写入字符串内容(字节数组) out.write(buffer, 0, len); } else { out.writeInt(-1); } } /** * 与 writeString(DataOutput, String) 方法相反,用于读取字符串类型数据 * * @param in * @return * @throws IOException * @see #writeString(DataOutput, String) */ private static final String readString(DataInput in) throws IOException { int length = in.readInt(); if (length == -1) return null; byte[] buffer = new byte[length]; in.readFully(buffer); // could/should use readFully(buffer,0,length)? return new String(buffer, "UTF-8"); } // getter / setter ... }
代码里实现了序列化和反序列化逻辑,Data2是一个POJO类,与上例中的Data类属性彻底同样,只是多了序列化和反序列化方法(这两个方法写在POJO类中的缘由是其序列化、反序列化有顺序要求,放在外面会难以控制)。 从实现代码中发现实际序列化、反序列化是由DataOutput
、DataInput
两个接口及其实现类来实现的,这些类彻底由JDK提供,并不依赖任何第三方的库,因为手动控制了序列化、反序列化,因此其性能和序列化后的大小控制都很是好框架
// 序列化的数据参考 JdkSerializeTest 中的Data对象 // 序列化、反序列化的过程都是一个字段一个字段的实现,虽然繁琐,但序列化后的大小和性能都比JDK原生序列化API强不少 byte[] buf = data.serialize(); // 测试序列化大小:JDK序列化后是947,这里只有204 assertEquals(204, buf.length); // 执行反序列化,注意读取的顺序与写入的顺序要一致 Data2 data2 = Data2.deserialize(buf); assertFalse(data == data2); assertEquals(data.getA(), data2.getA()); assertEquals(data.getA0(), data2.getA0()); // 因为浮点数在计算时会有偏差,这里第三个参数用于控制偏差 assertEquals(data.getC(), data2.getC(), 0.0); assertEquals(data.getC0(), data2.getC0(), 0.0); assertEquals(data.getE(), data2.getE()); assertEquals(data.isE0(), data2.isE0()); assertEquals(data.getF(), data2.getF()); assertEquals(data.getF0(), data2.getF0()); assertEquals(data.getG(), data2.getG()); assertEquals(data.getG0(), data2.getG0()); assertEquals(data.getH(), data2.getH()); assertEquals(data.getH0(), data2.getH0()); assertEquals(data.getI(), data2.getI()); assertEquals(data.getJ(), data2.getJ());
能够看出一样对象序列化后只有204个字节,约为以前的1/4,并且序列化的性能也调出不少,后面会给出简单对比。分布式
在一些开源框架中(如:Dubbo),也使用Hessian库(这里指的是Hessian2)来实现序列化。
// 执行序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(baos); hessian2Output.writeObject(data); hessian2Output.close(); // 获取字节数组前,必须先关闭Hessian2Output,不然取得字节数组长度为0(缘由暂不清楚) byte[] buf = baos.toByteArray(); baos.close(); // 测试断言 Assert.assertNotNull(buf); Assert.assertEquals(373, buf.length); System.out.println(new String(buf)); // 执行反序列化 Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf)); Data data2 = (Data) hessian2Input.readObject(); hessian2Input.close(); // 测试断言 assertFalse(data == data2); assertEquals(data.getA(), data2.getA()); assertEquals(data.getI(), data2.getI()); assertEquals(data.getJ(), data2.getJ());
相对JDK序列化和Hadoop序列化,其序列化后的数据大小居中,实际上性能也是居中的。但该库的优点在于,其跨语言的特性,也就是说能够向非Java语言的程序发送序列化数据,并能由对应语言的Hessian库实现反序列化。
下面使用10,000次循环序列化、反序列化(单线程)来测试三种序列化方式的耗时(该测试仅供参考,场景有限,并不能真的说明三种方式优劣程度)。
@Test public void performance() throws IOException, ClassNotFoundException { final int loop = 10_000; long time = System.currentTimeMillis(); for (int i = 0; i < loop; i++) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream output = new ObjectOutputStream(baos); output.writeObject(data); baos.close(); output.close(); byte[] buf = baos.toByteArray(); // 执行反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(buf); ObjectInputStream input = new ObjectInputStream(bais); input.readObject(); bais.close(); input.close(); } // loop = 10,000 -> 程序执行耗时:1037 毫秒! System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time)); }
@Test public void performance() throws IOException { final int loop = 10_000; long time = System.currentTimeMillis(); for (int i = 0; i < loop; i++) { // 执行序列化 byte[] buf = data.serialize(); // 执行反序列化 Data2.deserialize(buf); } // loop = 10,000 -> 程序执行耗时:75 毫秒! System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time)); }
public void performance() throws IOException { final int loop = 10_000; long time = System.currentTimeMillis(); for (int i = 0; i < loop; i++) { // 执行序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(baos); hessian2Output.writeObject(data); hessian2Output.close(); byte[] buf = baos.toByteArray(); // 执行反序列化 Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(buf)); hessian2Input.readObject(); hessian2Input.close(); } // loop = 10,000 -> 程序执行耗时:300 毫秒! System.out.println(String.format("程序执行耗时:%d 毫秒!", System.currentTimeMillis() - time)); }
结论(非权威,有兴趣的自行研究吧) | 循环次数 | jdk (947bytes) | hadoop (204bytes) | hessian (373bytes) | | - | - | - | - | | 10,000 | 1,037ms | 75ms | 300ms |
实际应用中,序列化可选方案不少,像Hadoop还能够用Avro、Protobuf来进行序列化,下面列出一些经常使用的序列化库:
序列化在分布式架构中(比较偏底层)是很重要的一环,好的序列化方案能够节省大量的带宽,而且提高程序处理速度。
后面列出的一些序列化方案本文未详细解释,这里先留个坑,后面将专门撰文来说解。
源码仓库: