为什么需要序列化
- 数据持久化(如session信息存储到redis)或在网络上传输(如RPC远程调用)
序列化要考虑的因素
- 性能:速度越快越好
- 序列化后字节大小:字节越小越好,节省带宽和存储器空间
- 兼容性:类的信息发生变化,旧的序列化数据是否能正常用新类反序列化,或者反之。如果序列化内容是放在内存并且每次发版(停服发版)都会清空,那么可以不考虑兼容,否则兼容性就要考虑。灰度发布之类的也要考虑兼容性。这里说的兼容性是指加减字段,不包括更改字段类型。
常见序列化方式
- JDK自带的ObjectInputStream和ObjectOutputStream,需要实现Serializable,需要兼容的话要写死servialVersionUID。性能低、体积大。
- 各类json(jackson, gson, fastjson),性能比jdk稍高,体积也稍小,对人友好,基本所有主流语言都支持,跨语言性非常好。兼容性好。但是类和字段的信息没有序列化进去,在反序列化的时候需要指定类名。
- hessian:性能和字节比jdk好,兼容性差。如果子类和父类有相同的属性名,那么在反序列化后会丢失字节,原因:hessian先写子类Field,再写父类Field,写值的顺序也一样,因为父类Field的值一般都是null,所以在反序列化的时候,总是把最后的父类的null值覆盖掉子类的值,具体原因参考:https://www.cnblogs.com/yfyzy/p/7197679.html。hessian的一些类不是public,不能继承,如果要改的话只能改源码了。
- hessian-lite:阿里dubbo项目里默认用的序列化协议,改自hessian,他解决了字节丢失问题,就是在获取所有Field后做下reverse操作,颠倒了Field的顺序。但是经过测试发现heissian-lite速度太慢了,见issue:https://github.com/dubbo/hessian-lite/issues/10
- kryo:速度和性能都很好,默认不兼容,不过通过设置CompatibleFieldSerializcer就能支持兼容,但是也不允许父类和子类有相同名字的属性,可以通过继承过滤掉同名属性。kryo可以参考官方文档,https://github.com/EsotericSoftware/kryo#compatiblefieldserializer-settings,很详细的。
- fst:性能和字节大小都是最优的,可惜兼容性要在字段上加@Version,只能增字段不能删,对业务开发侵入太大,如果不考虑兼容的话可以考虑用fst。参考:https://blog.csdn.net/dutlxq2014/article/details/86698268。wiki:https://github.com/RuedigerMoeller/fast-serialization/wiki
- 需要静态编译的,如果protobuf, thrift,适合内部系统之间RPC,本文不涉及这部分。
kryo目前的bug
- kryo不要每次都new Kryo(),这样性能太差,需要用ThreadLocal或池化存储kryo实例,不过目前发行版池化有个bug:https://github.com/EsotericSoftware/kryo/issues/642,每次池里取不到都会new一个出来,在还到池里的时候,如果池满了就会抛queue full异常。目前kryo池化还有一个bug,参考:https://github.com/EsotericSoftware/kryo/issues/664。只能自己实现池化。
- 序列化后如果bean的字段改了类型会导致jvm crash,虽说字段改类型不应该,但是导致jvm crash也是一个大问题。参考:https://github.com/EsotericSoftware/kryo/issues/663,能反序列化成功就是因为从序列化字节里拿到原来的类型,然后通过unsafe直接写内存。
性能和字节大小对比
SerializeBenchmarkTest3
测试类,对几种序列化方式进行了测试:
测试数据:
private Person getPerson() {
Person person = new Person();
person.setId(123L);
person.setName("你好啊");
person.setMarried(true);
person.setAge(22);
person.setDigits(Arrays.asList(1L, 3L, 100L));
Map<String, Double> scoreMap = new LinkedHashMap<>();
scoreMap.put("chinese", 90d);
scoreMap.put("english", 80.5d);
person.setScores(scoreMap);
Book book = new Book();
book.setId(99L);
book.setName("代码大全");
book.setPrice(56.00d);
person.setBook(book);
int friendsCount = 1000;
List<Person> friends = new ArrayList<>(friendsCount);
for (int i = 0; i < friendsCount; i++) {
Person friend = new Person();
friend.setId(Long.valueOf(i));
friend.setName(String.valueOf("我的朋友" + i));
friend.setMarried(i % 2 == 0 ? true : false);
friends.add(friend);
}
person.setFriends(friends);
return person;
}
test1
方法测试了序列化后自己大小和md5,测试结果如下:
2019-03-23 15:22:04,916 WARN [main] c.y.o.s.SerializeBenchmarkTest3.jdkPerformance(54) - jdk序列化后长度:52797, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,226 WARN [main] c.y.o.s.SerializeBenchmarkTest3.jsonPerformance(71) - json序列化后长度:59457, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,309 WARN [main] c.y.o.s.SerializeBenchmarkTest3.hessian2Performance(88) - hessian2序列化后长度:26124, 前后长度一致:false, md5一致:false,对象equals:false
2019-03-23 15:22:05,380 WARN [main] c.y.o.s.SerializeBenchmarkTest3.hessianLitePerformance(105) - hessian-lite序列化后长度:26144, 前后长度一致:false, md5一致:false,对象equals:true
2019-03-23 15:22:05,661 WARN [main] c.y.o.s.SerializeBenchmarkTest3.kryoPerformance(122) - kryo序列化后长度:28101, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,696 WARN [main] c.y.o.s.SerializeBenchmarkTest3.fstPerformance(139) - fst序列化后长度:33839, 前后长度一致:true, md5一致:true,对象equals:true
由于Person类继承了Human类,2个类都有同名属性id,hessian2在序列化的时候存在bug导致丢失数据,奇怪的是hessian-lite虽然解决了这个bug,但是前后序列化字节长度却不相等。
从上面结果可以看出,在小数据量场景下,hessian2及hessian-lite在体积上占有小优势,kryo、fst次之,jdk和json最差。
然后对上面的数据做10000次序列化和反序列化,结果如下:
14次YGC
13.658: [GC (Allocation Failure) [PSYoungGen: 682646K->233K(691200K)] 686763K->4350K(2089472K), 0.0018965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-03-23 15:31:22,073 WARN [main] c.y.o.s.SerializeBenchmarkTest3.jdkPerformance(61) - jdk序列化、反序列化10000次耗时13699
9次YGC
19.258: [GC (Allocation Failure) [PSYoungGen: 687902K->318K(693248K)] 692530K->4946K(2091520K), 0.0006095 secs] [Times: user=0.06 sys=0.00, real=0.00 secs]
2019-03-23 15:31:27,321 WARN [main] c.y.o.s.SerializeBenchmarkTest3.jsonPerformance(78) - json序列化、反序列化10000次耗时5245
5次YGC
24.824: [GC (Allocation Failure) [PSYoungGen: 689776K->121K(694784K)] 694539K->4909K(2093056K), 0.0007943 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-03-23 15:31:32,671 WARN [main] c.y.o.s.SerializeBenchmarkTest3.hessian2Performance(95) - hessian2序列化、反序列化10000次耗时5349
11次YGC
35.886: [GC (Allocation Failure) [PSYoungGen: 694368K->64K(696320K)] 699315K->5011K(2094592K), 0.0073285 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
2019-03-23 15:31:43,688 WARN [main] c.y.o.s.SerializeBenchmarkTest3.hessianLitePerformance(112) - hessian-lite序列化、反序列化10000次耗时11017
3次YGC
39.634: [GC (Allocation Failure) [PSYoungGen: 694880K->64K(696832K)] 700003K->5203K(2095104K), 0.0007231 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-03-23 15:31:48,061 WARN [main] c.y.o.s.SerializeBenchmarkTest3.kryoPerformance(129) - kryo序列化、反序列化10000次耗时4373
3次YGC
43.816: [GC (Allocation Failure) [PSYoungGen: 694945K->193K(696832K)] 700308K->5556K(2095104K), 0.0007227 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-03-23 15:31:51,626 WARN [main] c.y.o.s.SerializeBenchmarkTest3.fstPerformance(146) - fst序列化、反序列化10000次耗时3564
jvm参数:-Xms2g -Xmx2g -XX:+PrintGCTimeStamps -XX:+PrintGCDetails
可以看出fst最快,kryo次之,json、hessian2速度还不错,但是hessian-lite和jdk基本上一样慢。
最佳实践
- 序列化的类最好实现Serializable接口,并写死serialVersionUID
- 序列化的类可以加减字段,但是最好不要改字段类型
- 如果是开放出去的api,最好采用可读性好、适合web的json,兼容性也好,和语言没有耦合,就是浪费带宽
- 如果是内部RPC,可以采用fst和kryo,或者protobuf, thrift。如果要兼容多版本,fst就不太适合
- 如果有持久化需求,需要考虑到兼容性,可以采用kryo, json
序列化工具类:
static MzKryoPool<Kryo> kryoPool = new MzKryoPool<Kryo>(100);
static FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();
public static <T> byte[] serializeWithJdk(T object) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
byte[] bytes = byteArrayOutputStream.toByteArray();
objectOutputStream.close();
return bytes;
} catch (IOException e) {
throw new OperationException("serialize with jdk fail: " + e.getMessage(), e);
}
}
public static Object deserializeWithJdk(byte[] bytes) {
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
objectInputStream.close();
return object;
} catch (ClassNotFoundException | IOException e) {
throw new OperationException("deserialize with jdk fail: " + e.getMessage(), e);
}
}
public static byte[] serializeWithJson(Object object) {
return JSON.toJSONBytes(object);
}
public static <T> T deserializeWithJson(byte[] bytes, Class<T> cls) {
return JSON.parseObject(bytes, cls);
}
public static byte[] serializeWithHessian2(Object object) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
Hessian2Output hessianOutput = new Hessian2Output(byteArrayOutputStream);
hessianOutput.startMessage();
hessianOutput.writeObject(object);
hessianOutput.completeMessage();
hessianOutput.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new OperationException("serialize with hessian2 fail: " + e.getMessage(), e);
}
}
public static Object deserializeWithHessian2(byte[] bytes) {
try {
Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(bytes));
hessian2Input.startMessage();
Object o = hessian2Input.readObject();
hessian2Input.completeMessage();
hessian2Input.close();
return o;
} catch (IOException e) {
throw new OperationException("deserialize with hessian2 fail: " + e.getMessage(), e);
}
}
public static byte[] serializeWithHessianLite(Object object) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
com.alibaba.com.caucho.hessian.io.Hessian2Output hessian2Output = new com.alibaba.com.caucho.hessian.io.Hessian2Output(byteArrayOutputStream);
hessian2Output.startMessage();
hessian2Output.writeObject(object);
hessian2Output.completeMessage();
hessian2Output.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new OperationException("serialize with hessian-lite fail: " + e.getMessage(), e);
}
}
public static Object deserializeWithHessianLite(byte[] bytes) {
try {
com.alibaba.com.caucho.hessian.io.Hessian2Input hessian2Input = new com.alibaba.com.caucho.hessian.io.Hessian2Input(new ByteArrayInputStream(bytes));
hessian2Input.startMessage();
Object o = hessian2Input.readObject();
hessian2Input.completeMessage();
hessian2Input.close();
return o;
} catch (IOException e) {
throw new OperationException("deserialize with hessian-lite fail: " + e.getMessage(), e);
}
}
public static byte[] serializeWithKryo(Object obj) {
Kryo kryo = kryoPool.obtain();
//initial 4k, max 10M
try (Output output = new Output(4096, 10 * 1024 * 1024);) {
kryo.writeClassAndObject(output, obj);
return output.toBytes();
} catch (Exception e) {
throw new OperationException("deserialize with kryo fail: " + e.getMessage(), e);
} finally {
kryoPool.free(kryo);
}
}
public static Object deserializeWithKryo(byte[] bytes) {
Kryo kryo = kryoPool.obtain();
try (Input input = new Input(bytes)) {
return kryo.readClassAndObject(input);
} catch (Exception e) {
throw new OperationException("deserialize with kryo fail: " + e.getMessage(), e);
} finally {
kryoPool.free(kryo);
}
}
public static byte[] serializeWithFst(Object obj) {
return fst.asByteArray(obj);
}
public static Object deserializeWithFst(byte[] bytes) {
return fst.asObject(bytes);
}