干货系列性能篇之——序列化
序列化方案
- java rmi采用的是java序列化
- spring cloud采用的是json序列化
- dubbo虽然兼容java序列化,但默认使用的是hessian序列化
java序列化
原理
serializable
- jdk提供了输入流对象objectinputstream和输出流对象objectoutputstream
- 它们只能对实现了serializable接口的类的对象进行序列化和反序列化
// 只能对实现了serializable接口的类的对象进行序列化 // java.io.notserializableexception: java.lang.object objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(new object()); oos.close();
transient
- objectoutputstream的默认序列化方式,仅对对象的非transient的实例变量进行序列化
- 不会序列化对象的transient的实例变量,也不会序列化静态变量
@getter public class a implements serializable { private transient int f1 = 1; private int f2 = 2; @getter private static final int f3 = 3; } // 序列化 // 仅对对象的非transient的实例变量进行序列化 a a1 = new a(); objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(a1); oos.close(); // 反序列化 objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); a a2 = (a) ois.readobject(); log.info("f1={}, f2={}, f3={}", a2.getf1(), a2.getf2(), a2.getf3()); // f1=0, f2=2, f3=3 ois.close();
serialversionuid
- 在实现了serializable接口的类的对象中,会生成一个serialversionuid的版本号
- 在反序列化过程中用来验证序列化对象是否加载了反序列化的类
- 如果是具有相同类名的不同版本号的类,在反序列化中是无法获取对象的
@data @allargsconstructor public class b implements serializable { private static final long serialversionuid = 1l; private int id; } @test public void test3() throws exception { // 序列化 b b1 = new b(1); objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(b1); oos.close(); } @test public void test4() throws exception { // 如果先将b的serialversionuid修改为1,直接反序列化磁盘上的文件,会报异常 // java.io.invalidclassexception: xxx.b; local class incompatible: stream classdesc serialversionuid = 0, local class serialversionuid = 1 objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); b b2 = (b) ois.readobject(); ois.close(); }
writeobject/readobject
具体实现序列化和反序列化的是writeobject和readobject
@data @allargsconstructor public class student implements serializable { private long id; private int age; private string name; // 只序列化部分字段 private void writeobject(objectoutputstream outputstream) throws ioexception { outputstream.writelong(id); outputstream.writeobject(name); } // 按序列化的顺序进行反序列化 private void readobject(objectinputstream inputstream) throws ioexception, classnotfoundexception { id = inputstream.readlong(); name = (string) inputstream.readobject(); } } student s1 = new student(1, 12, "bob"); objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(s1); oos.close(); objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); student s2 = (student) ois.readobject(); log.info("s2={}", s2); // s2=student(id=1, age=0, name=bob) ois.close();
writereplace/readresolve
- writereplace:用在序列化之前替换序列化对象
- readresolve:用在反序列化之后对返回对象进行处理
// 反序列化会通过反射调用无参构造器返回一个新对象,破坏单例模式 // 可以通过readresolve()来解决 public class singleton1 implements serializable { private static final singleton1 singleton_1 = new singleton1(); private singleton1() { } public static singleton1 getinstance() { return singleton_1; } } singleton1 s1 = singleton1.getinstance(); objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(s1); oos.close(); objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); singleton1 s2 = (singleton1) ois.readobject(); log.info("{}", s1 == s2); // false ois.close();
public class singleton2 implements serializable { private static final singleton2 singleton_2 = new singleton2(); private singleton2() { } public static singleton2 getinstance() { return singleton_2; } public object writerepalce() { // 序列化之前,无需替换 return this; } private object readresolve() { // 反序列化之后,直接返回单例 return getinstance(); } } singleton2 s1 = singleton2.getinstance(); objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(s1); oos.close(); objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); singleton2 s2 = (singleton2) ois.readobject(); log.info("{}", s1 == s2); // true ois.close();
缺陷
无法跨语言
java序列化只适用于基于java语言实现的框架
易被攻击
1.java序列化是不安全的
- java官网:对不信任数据的反序列化,本质上来说是危险的,应该予以回避
2.objectinputstream.readobject()
- 将类路径上几乎所有实现了serializable接口的对象都实例化!!
- 这意味着:在反序列化字节流的过程中,该方法可以执行任意类型的代码,非常危险
3.对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击
- 攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中进行反序列化
- 这会导致hasecode方法被调用的次数呈次方爆发式增长,从而引发栈溢出异常
4.很多序列化协议都制定了一套数据结构来保存和获取对象,如json序列化、protocolbuf
- 它们只支持一些基本类型和数组类型,可以避免反序列化创建一些不确定的实例
int itcount = 27; set root = new hashset(); set s1 = root; set s2 = new hashset(); for (int i = 0; i < itcount; i++) { set t1 = new hashset(); set t2 = new hashset(); t1.add("foo"); // 使t2不等于t1 s1.add(t1); s1.add(t2); s2.add(t1); s2.add(t2); s1 = t1; s2 = t2; } objectoutputstream oos = new objectoutputstream(new fileoutputstream(file_path)); oos.writeobject(root); oos.close(); long start = system.currenttimemillis(); objectinputstream ois = new objectinputstream(new fileinputstream(file_path)); ois.readobject(); log.info("take : {}", system.currenttimemillis() - start); ois.close(); // itcount - take // 25 - 3460 // 26 - 7346 // 27 - 11161
序列化后的流太大
1.序列化后的二进制流大小能体现序列化的能力
2.序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高
- 如果进行网络传输,则占用的带宽就越多,影响到系统的吞吐量
3.java序列化使用objectoutputstream来实现对象转二进制编码,可以对比bio中的 bytebuffer实现的二进制编码
@data class user implements serializable { private string username; private string password; } user user = new user(); user.setusername("test"); user.setpassword("test"); // objectoutputstream bytearrayoutputstream os = new bytearrayoutputstream(); objectoutputstream oos = new objectoutputstream(os); oos.writeobject(user); log.info("{}", os.tobytearray().length); // 107 // nio bytebuffer bytebuffer bytebuffer = bytebuffer.allocate(2048); byte[] username = user.getusername().getbytes(); byte[] password = user.getpassword().getbytes(); bytebuffer.putint(username.length); bytebuffer.put(username); bytebuffer.putint(password.length); bytebuffer.put(password); bytebuffer.flip(); log.info("{}", bytebuffer.remaining()); // 16
序列化速度慢
- 序列化速度是体现序列化性能的重要指标
- 如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间
int count = 10_0000; user user = new user(); user.setusername("test"); user.setpassword("test"); // objectoutputstream long t1 = system.currenttimemillis(); for (int i = 0; i < count; i++) { bytearrayoutputstream os = new bytearrayoutputstream(); objectoutputstream oos = new objectoutputstream(os); oos.writeobject(user); oos.flush(); oos.close(); byte[] bytes = os.tobytearray(); os.close(); } long t2 = system.currenttimemillis(); log.info("{}", t2 - t1); // 731 // nio bytebuffer long t3 = system.currenttimemillis(); for (int i = 0; i < count; i++) { bytebuffer bytebuffer = bytebuffer.allocate(2048); byte[] username = user.getusername().getbytes(); byte[] password = user.getpassword().getbytes(); bytebuffer.putint(username.length); bytebuffer.put(username); bytebuffer.putint(password.length); bytebuffer.put(password); bytebuffer.flip(); byte[] bytes = new byte[bytebuffer.remaining()]; } long t4 = system.currenttimemillis(); log.info("{}", t4 - t3); // 182
protobuf
- protobuf是由google推出且支持多语言的序列化框架
- 在序列化框架性能测试报告中,protobuf无论编解码耗时,还是二进制流压缩大小,都表现很好
- protobuf以一个.proto后缀的文件为基础,该文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件
- 在序列化该数据对象的时候,protobuf通过.proto文件描述来生成protocol buffers格式的编码
存储格式
- protocol buffers是一种轻便高效的结构化数据存储格式
- protocol buffers使用t-l-v(标识-长度-字段值)的数据格式来存储数据
- t代表字段的正数序列(tag)
- protocol buffers将对象中的字段与正数序列对应起来,对应关系的信息是由生成的代码来保证的
- 在序列化的时候用整数值来代替字段名称,传输流量就可以大幅缩减
- l代表value的字节长度,一般也只占用一个字节
- v代表字段值经过编码后的值
- 这种格式不需要分隔符,也不需要空格,同时减少了冗余字段名
编码方式
1.protobuf定义了一套自己的编码方式,几乎可以映射java/python等语言的所有基础数据类型
2.不同的编码方式可以对应不同的数据类型,还能采用不同的存储格式
3.对于varint编码的数据,由于数据占用的存储空间是固定的,因此不需要存储字节长度length,存储方式采用t-v
4.varint编码是一种变长的编码方式,每个数据类型一个字节的最后一位是标志位(msb)
- 0表示当前字节已经是最后一个字节
- 1表示后面还有一个字节
5.对于int32类型的数字,一般需要4个字节表示,如果采用varint编码,对于很小的int类型数字,用1个字节就能表示
- 对于大部分整数类型数据来说,一般都是小于256,所以这样能起到很好的数据压缩效果
编解码
- protobuf不仅压缩存储数据的效果好,而且编解码的性能也是很好的
- protobuf的编码和解码过程结合.proto文件格式,加上protocol buffers独特的编码格式
- 只需要简单的数据运算以及位移等操作就可以完成编码和解码
我是小架,需要java学习进阶架构资料。加我的交流群
772300343 即可领取!
我们下篇文章见!
感谢!
下一篇: linux下安装python3和pip3