在分佈式架構中,序列化是分佈式的基礎構成之一,咱們須要把單臺設備上的數據經過序列化(編碼、壓縮)後經過網絡傳輸給網絡中的其它設備,從而實現信息交換。 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來進行序列化,下面列出一些經常使用的序列化庫:
序列化在分佈式架構中(比較偏底層)是很重要的一環,好的序列化方案能夠節省大量的帶寬,而且提高程序處理速度。
後面列出的一些序列化方案本文未詳細解釋,這裏先留個坑,後面將專門撰文來說解。
源碼倉庫: