點選上方“芋道原始碼”,選擇“置頂公眾號”
技術文章第一時間送達!
原始碼精品專欄
一年前,筆者剛剛接觸RPC框架,從單體式應用向分散式應用的變革無疑是讓人興奮的,同時也對RPC背後到底做了哪些工作產生了興趣,但其底層的設計對新手而言並不是很友好,其涉及的一些常用技術點都有一定的門檻。如傳輸層常常使用的netty,之前完全沒聽過,想要學習它,需要掌握前置知識點nio;協議層,包括了很多自定義的協議,而每個RPC框架的實現都有差異;代理層的動態代理技術,如jdk動態代理,雖然實戰經驗不多,但至少還算會用,而cglib則又有一個盲區;序列化層倒還算是眾多層次中相對簡單的一環,但RPC為了追求可擴充套件性,效能等諸多因素,通常會支援多種序列化方式以供使用者插拔使用,一些常用的序列化方案hessian,kryo,Protobuf又得熟知…
這個系列打算就RPC框架涉及到的一些知識點進行探討,本篇先從序列化層的一種選擇–kryo開始進行介紹。
序列化概述
大白話介紹下RPC中序列化的概念,可以簡單理解為物件–>位元組的過程,同理,反序列化則是相反的過程。為什麼需要序列化?因為網路傳輸只認位元組。所以互信的過程依賴於序列化。有人會問,FastJson轉換成字串算不算序列化?物件持久化到資料庫算不算序列化?沒必要較真,廣義上理解即可。
JDK序列化
可能你沒用過kryo,沒用過hessian,但你一定用過jdk序列化。我最早接觸jdk序列化,是在大二的JAVA大作業中,《XX管理系統》需要把物件儲存到檔案中(那時還沒學資料庫),jdk原生支援的序列化方式用起來也很方便。
class Student implements Serializable{
private String name;
}
class Main{
public static void main(String[] args) throws Exception{
// create a Student
Student st = new Student("kirito");
// serialize the st to student.db file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.db"));
oos.writeObject(st);
oos.close();
// deserialize the object from student.db
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.db"));
Student kirito = (Student) ois.readObject();
ois.close();
// assert
assert "kirito".equals(kirito.getName());
}
}
Student物體類需要實現Serializable介面,以告知其可被序列化。
序列化協議的選擇通常有下列一些常用的指標:
-
通用性。是否只能用於java間序列化/反序列化,是否跨語言,跨平臺。
-
效能。分為空間開銷和時間開銷。序列化後的資料一般用於儲存或網路傳輸,其大小是很重要的一個引數;解析的時間也影響了序列化協議的選擇,如今的系統都在追求極致的效能。
-
可擴充套件性。系統升級不可避免,某一物體的屬性變更,會不會導致反序列化異常,也應該納入序列化協議的考量範圍。
-
易用性。API使用是否複雜,會影響開發效率。
容易用的模型通常效能不好,效能好的模型通常用起來都比較麻煩。顯然,JDK序列化屬於前者。我們不過多介紹它,直接引入今天的主角kryo作為它的替代品。
Kryo入門
引入依賴
com.esotericsoftware
由於其底層依賴於ASM技術,與Spring等框架可能會發生ASM依賴的版本衝突(檔案中表示這個衝突還挺容易出現)所以提供了另外一個依賴以供解決此問題
com.esotericsoftware
快速入門
class Student implements Serializable{
private String name;
}
public class Main {
public static void main(String[] args) throws Exception{
Kryo kryo = new Kryo();
Output output = new Output(new FileOutputStream("student.db"));
Student kirito = new Student("kirito");
kryo.writeObject(output, kirito);
output.close();
Input input = new Input(new FileInputStream("student.db"));
Student kiritoBak = kryo.readObject(input, Student.class);
input.close();
assert "kirito".equals(kiritoBak.getName());
}
}
不需要註釋也能理解它的執行流程,和jdk序列化差距並不是很大。
三種讀寫方式
Kryo共支援三種讀寫方式
-
如果知道class位元組碼,並且物件不為空
kryo.writeObject(output, someObject);
// ...
SomeClass someObject = kryo.readObject(input, SomeClass.class);
快速入門中的序列化/反序列化的方式便是這一種。而Kryo考慮到someObject可能為null,也會導致傳回的結果為null,所以提供了第二套讀寫方式。
-
如果知道class位元組碼,並且物件可能為空
kryo.writeObjectOrNull(output, someObject);
// ...
SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);
但這兩種方法似乎都不能滿足我們的需求,在RPC呼叫中,序列化和反序列化分佈在不同的端點,物件的型別確定,我們不想依賴於手動指定引數,最好是…emmmmm…將位元組碼的資訊直接存放到序列化結果中,在反序列化時自行讀取位元組碼資訊。Kryo考慮到了這一點,於是提供了第三種方式。
-
如果實現類的位元組碼未知,並且物件可能為null
kryo.writeClassAndObject(output, object);
// ...
Object object = kryo.readClassAndObject(input);
if (object instanceof SomeClass) {
// ...
}
我們犧牲了一些空間一些效能去存放位元組碼資訊,但這種方式是我們在RPC中應當使用的方式。
我們關心的問題
繼續介紹Kryo特性之前,不妨讓我們先思考一下,一個序列化工具或者一個序列化協議,應當需要考慮哪些問題。比如,支援哪些型別的序列化?迴圈取用會不會出現問題?在某個類增刪欄位之後反序列化會報錯嗎?等等等等….
帶著我們考慮到的這些疑惑,以及我們暫時沒考慮到的,但Kryo幫我們考慮到的,來看看Kryo到底支援哪些特性。
支援的序列化型別
boolean | Boolean | byte | Byte | char |
---|---|---|---|---|
Character | short | Short | int | Integer |
long | Long | float | Float | double |
Double | byte[] | String | BigInteger | BigDecimal |
Collection | Date | Collections.emptyList | Collections.singleton | Map |
StringBuilder | TreeMap | Collections.emptyMap | Collections.emptySet | KryoSerializable |
StringBuffer | Class | Collections.singletonList | Collections.singletonMap | Currency |
Calendar | TimeZone | Enum | EnumSet |
表格中支援的型別一覽無餘,這都是其預設支援的。
Kryo kryo = new Kryo();
kryo.addDefaultSerializer(SomeClass.class, SomeSerializer.class);
這樣的方式,也可以為一個Kryo實體擴充套件序列化器。
總體而言,Kryo支援以下的型別:
-
列舉
-
集合、陣列
-
子類/多型
-
迴圈取用
-
內部類
-
泛型
但需要註意的是,Kryo不支援Bean中增刪欄位。如果使用Kryo序列化了一個類,存入了Redis,對類進行了修改,會導致反序列化的異常。
另外需要註意的一點是使用反射建立的一些類序列化的支援。如使用Arrays.asList();建立的List物件,會引起序列化異常。
Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): java.util.Arrays$ArrayList
但new ArrayList()建立的List物件則不會,使用時需要註意,可以使用第三方庫對Kryo進行序列化型別的擴充套件。如https://github.com/magro/kryo-serializers所提供的。
不支援不包含無參建構式類的反序列化,嘗試反序列化一個不包含無參建構式的類將會得到以下的異常:
Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): moe.cnkirito.Xxx
保證每個類具有無參建構式是應當遵守的程式設計規範,但實際開發中一些第三庫的相關類不包含無參構造,的確是有點麻煩。
執行緒安全
Kryo是執行緒不安全的,意味著每當需要序列化和反序列化時都需要實體化一次,或者藉助ThreadLocal來維護以保證其執行緒安全。
private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
// configure kryo instance, customize settings
return kryo;
};
};
// Somewhere else, use Kryo
Kryo k = kryos.get();
...
Kryo相關配置引數詳解
每個Kryo實體都可以擁有兩個配置引數,這值得被拉出來單獨聊一聊。
kryo.setRegistrationRequired(false);//關閉註冊行為
kryo.setReferences(true);//支援迴圈取用
Kryo支援對註冊行為,如 kryo.register(SomeClazz.class);
,這會賦予該Class一個從0開始的編號,但Kryo使用註冊行為最大的問題在於,其不保證同一個Class每一次註冊的號碼想用,這與註冊的順序有關,也就意味著在不同的機器、同一個機器重啟前後都有可能擁有不同的編號,這會導致序列化產生問題,所以在分散式專案中,一般關閉註冊行為。
第二個註意點在於迴圈取用,Kryo為了追求高效能,可以關閉迴圈取用的支援。不過我並不認為關閉它是一件好的選擇,大多數情況下,請保持 kryo.setReferences(true)
。
常用Kryo工具類
public class KryoSerializer {
public byte[] serialize(Object obj) {
Kryo kryo = kryoLocal.get();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);//<1>
kryo.writeClassAndObject(output, obj);//<2>
output.close();
return byteArrayOutputStream.toByteArray();
}
public <T> T deserialize(byte[] bytes) {
Kryo kryo = kryoLocal.get();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream);// <1>
input.close();
return (T) kryo.readClassAndObject(input);//<2>
}
private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {//<3>
@Override
protected Kryo initialValue() {
Kryo kryo = new Kryo();
kryo.setReferences(true);//預設值為true,強調作用
kryo.setRegistrationRequired(false);//預設值為false,強調作用
return kryo;
}
};
}
<1> Kryo的Input和Output接收一個InputStream和OutputStream,Kryo通常完成位元組陣列和物件的轉換,所以常用的輸入輸出流實現為ByteArrayInputStream/ByteArrayOutputStream。
<2> writeClassAndObject和readClassAndObject配對使用在分散式場景下是最常見的,序列化時將位元組碼存入序列化結果中,便可以在反序列化時不必要傳入位元組碼資訊。
<3> 使用ThreadLocal維護Kryo實體,這樣減少了每次使用都實體化一次Kryo的開銷又可以保證其執行緒安全。
參考文章
https://github.com/EsotericSoftware/kryo
Kryo 使用指南
序列化與反序列化
更多的序列化方案,和RPC其他層次中會涉及到的技術,在後續的文章中進行逐步介紹。