欢迎来到窥视未来的博客

Fork me on GitHub

分布式系统的基石序列化和反序列化

了解序列化的意义

 

 

序列化能解决什么问题?

 

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在 JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能

简单来说
 序列化是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化

​ 反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化

 

序列化面临的挑战

评价一个序列化算法优劣的两个重要指标是:序列化以后的数据大小;序列化操作本身的速度及系统资源开销(CPU、内存);Java 语言本身提供了对象序列化机制,也是 Java 语言本身最重要的底层机制之一,Java 本身提供的序列化机制存在两个问题

  1. 序列化的数据比较大,传输效率低

  2. 其他语言无法识别和对接

java 语言本身提供了对象序列化机制,也是 Java 语言本身最重要的底层机制之一,Java本身提供的序列化机制存在两个问题

  • 序列化的数据比较大,传输效率低

  • 其他语言无法识别和对接

基于JDK序列化方式实现 JDK 提供了 Java 对 象 的 序 列 化 方 式 , 主 要 通 过 输 出 流java.io.ObjectOutputStream 和对象输入流 java.io.ObjectInputStream来实现。其中,被序列化的对象需要实现java.io.Serializable接口

Serizlization

  1. 数据比较大

  2. 语言限制(跨语言跨平台)

  3. xml(soap)

提供序列化接口

 User实体类

 1 /**
 2  * @Description:    person实体类
 3  * @CreateDate:     2020/12/20 12:45
 4  * @UpdateDate:     2020/12/20 12:45
 5  * @UpdateRemark:   修改内容
 6  * @Version:        1.0
 7  */
 8 public class User implements Serializable {
 9 
10     private  String name ;
11     private  int age;
12     private  String sex;
13 
14     public String getName() {
15         return name;
16     }
17 
18     public void setName(String name) {
19         this.name = name;
20     }
21 
22     public int getAge() {
23         return age;
24     }
25 
26     public void setAge(int age) {
27         this.age = age;
28     }
29 
30     public String getSex() {
31         return sex;
32     }
33 
34     public void setSex(String sex) {
35         this.sex = sex;
36     }
37 
38      @Override
39     public String toString() {
40         return "User{" +
41                 "name='" + name + '\'' +
42                 ", age=" + age +
43                 ", hobby='" + hobby + '\'' +
44                 ", sex='" + sex + '\'' +
45                 '}';
46     }
47 }
View Code

 

 

 1 /**
 2  * @Description:    person实体类
 3  * @CreateDate:     2020/12/20 12:45
 4  * @UpdateDate:     2020/12/20 12:45
 5  * @UpdateRemark:   修改内容
 6  * @Version:        1.0
 7  */
 8 public class User implements Serializable {
 9 
10     private  String name ;
11     private  int age;
12     private  String sex;
13 
14     public String getName() {
15         return name;
16     }
17 
18     public void setName(String name) {
19         this.name = name;
20     }
21 
22     public int getAge() {
23         return age;
24     }
25 
26     public void setAge(int age) {
27         this.age = age;
28     }
29 
30     public String getSex() {
31         return sex;
32     }
33 
34     public void setSex(String sex) {
35         this.sex = sex;
36     }
37 
38      @Override
39     public String toString() {
40         return "User{" +
41                 "name='" + name + '\'' +
42                 ", age=" + age +
43                 ", hobby='" + hobby + '\'' +
44                 ", sex='" + sex + '\'' +
45                 '}';
46     }
47 }
View Code

java序列化

 1 /**
 2  * @Description: java序列化的方式
 3  * @CreateDate: 2020/12/20 12:34
 4  * @UpdateDate: 2020/12/20 12:34
 5  * @UpdateRemark: 修改内容
 6  * @Version: 1.0
 7  */
 8 public class JavaSerializer implements ISeralizer {
 9     @Override
10     public <T> byte[] serializer(T obj) {
11         ObjectOutputStream oos = null;
12         ByteArrayOutputStream byteArrayOutputStream = null;
13         try {
14             byteArrayOutputStream = new ByteArrayOutputStream();
15             oos = new ObjectOutputStream(byteArrayOutputStream);
16             oos.writeObject(obj);
17             return byteArrayOutputStream.toByteArray();
18         } catch (IOException e) {
19             e.printStackTrace();
20         } finally {
21             if (oos != null) {
22                 try {
23                     oos.close();
24                 } catch (IOException e) {
25                     e.printStackTrace();
26                 }
27             }
28             if (byteArrayOutputStream != null) {
29                 try {
30                     byteArrayOutputStream.close();
31                 } catch (IOException e) {
32                     e.printStackTrace();
33                 }
34             }
35         }
36         return new byte[0];
37     }
38 
39     @Override
40     public <T> T deSeralizer(byte[] bytes, Class<T> clazz) {
41         ByteArrayInputStream byteArrayInputStream = null;
42         ObjectInputStream objectInputStream = null;
43 
44         try {
45             byteArrayInputStream = new ByteArrayInputStream(bytes);
46             objectInputStream = new ObjectInputStream(byteArrayInputStream);
47             return (T) objectInputStream.readObject();
48         } catch (IOException | ClassNotFoundException e) {
49             e.printStackTrace();
50         } finally {
51             if (byteArrayInputStream != null) {
52                 try {
53                     byteArrayInputStream.close();
54                 } catch (IOException e) {
55                     e.printStackTrace();
56                 }
57             }
58             if (objectInputStream != null) {
59                 try {
60                     objectInputStream.close();
61                 } catch (IOException e) {
62                     e.printStackTrace();
63                 }
64             }
65         }
66         return null;
67     }
68 }
View Code

 

测试类

 1 /**
 2  * @Description:    测试类
 3  * @CreateDate:     2020/12/20 12:52
 4  * @UpdateDate:     2020/12/20 12:52
 5  * @UpdateRemark:   修改内容
 6  * @Version:        1.0
 7  */
 8 public class App {
 9     public static void main(String[] args) {
10         //java 序列化
11         ISeralizer seralizer = new JavaSerializer();
12         User user = new User();
13         user.setName("zhangsan");
14         user.setAge(20);
15         byte[] javaBytes = seralizer.serializer(user);
16         User user = seralizer.deSeralizer(javaBytes, User.class);
17 
18         System.out.println(user);
19 
20 
21     }
22 }
View Code

输出结果

Person{name='zhangsan', age=20, sex='null', email=Email{content='44834191@qq.com'}}

序列化的高阶认识

- serialVersionUID的作用
Java的序列化机制是`通过判断类的serialVersionUID来验证版本一致性的`。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是`

  InvalidCastException

 

如果没`有为指定的 class 配置 serialVersionUID,那么 java 编译器会自动给这个 class 进行一个摘要算法,类似于指纹算法`, 只要这个文件有任何改动,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的

- serialVersionUID 有两种显示的生成方式

一是默认的 1L ,比 如: private static final long serialVersionUID = 1L;
二是根据`类名、接口名、成员方法及属性`等来生成一个`64 位的哈希字段

当实现 java.io.Serializable 接 口 的 类 没 有 显 式 地 定 义 一 个serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,如果Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。

- 静态变量序列化

序列化保存的是对象的状态,静态变量属于类的状态,**因此 序列化并不保存静态变量。**

1 /**
2  * 在 Person 中添加一个全局的静态变量 num , 在执行序列化以后修改num 的值为 10, 
3  * 然后通过反序列化以后得到的对象去输出 num 的值
4  */
5 public static int num = 5;
View Code

 

 1 public static void main(String[] args) {
 2         //java 序列化
 3         ISeralizer seralizer = new JavaFileSerializer();
 4         User user = new User();
 5         user.setName("zhangsan");
 6         user.setSex("男");
 7         user.setAge(20);
 8         user.setHobby("haha");
 9         User.num=10;
10 //        byte[] javaBytes = seralizer.serializer(person);
11         byte[] serializer = seralizer.serializer(user);
12         User user1 = seralizer.deSerializer(null, User.class);
13 ////    //  最后的输出是 10,理论上打印的 num 是从读取的对象里获得的,应该是保存时的状态才对。
14 //        之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
15         System.out.println(user1+"->"+User.num);
16 
17 
18     }
View Code

 

- 父类的序列化

1.`子 类 继 承 该 父 类 并 且 实 现 了 序 列 化,此时父类并没有实现序列化`,在反序列化该子类后,是`没办法获取到父类的属性值的`
2.当一个`父类实现序列化,子类自动实现序列化`,不需要再显示实现Serializable 接口
3.`当一个对象的实例变量引用了其他对象,序列化该对象时 也会把引用对象进行序列化,但是前提是该引用对象必须实现序列化接口

 

- Transient 关键字

控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值, 如 int 型的是 0,对象型的是 null

 1 /**
 2  * 在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,
 3  *如 int 型的是 0 对象型的是 null
 4  */
 5 private transient  String hobby;
 6 
 7 // 绕开 transient 机制的办法 
 8 private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
 9         objectInputStream.defaultReadObject();
10         this.hobby = (String) objectInputStream.readObject();
11     }
12 
13 private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
14     objectOutputStream.defaultWriteObject();
15     objectOutputStream.writeObject(this.hobby);
16 }
View Code

 

绕开 transient 机制的办法 实体需要实现writeObject(ObjectOutputStream obj) readObject( ObjectInputStream objectInputStream ))的方法 比如 hobby还是要序列化,需要手动实现这两个方法:

注意:writeObject和 readObject 这两个私有的方法,既不属于 Object、也不是 Serializable,为什么能够在序列化的时候被调用呢?原因是,ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为 ObjectOutputStream使用getPrivateMethod,所以这些方法必须声明为 priate 以至于供ObjectOutputStream 来使用

序列化存储规则

 1 /**
 2  *  同一对象两次(开始写入文件到最终关闭流这个过程算一次,上面的演示效果是不关闭流的情况才能演示出效果)写入文件,打印出写入
 3  * 次对象后的存储大小和写入两次后的存储大小,第二次写入对象时文件只增加了 5 字节
 4  */
 5 public class StroreRuleDemo {
 6     public static void main(String[] args) throws IOException {
 7         ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("User.txt"));
 8         User user = new User();
 9         user.setName("sunkang");
10         //第一次写入
11         objectOutputStream.writeObject(user);
12         objectOutputStream.flush();
13         System.out.println(new File("User.txt").length());
14         //第二次写入同一个对象
15         objectOutputStream.writeObject(user);
16         objectOutputStream.close();
17         System.out.println(new File("User.txt").length());
18     }
19 }
View Code

 

Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系.该存储规则极大的节省了存储空间。

序列化实现深克隆

​ 在 Java 中存在一个 Cloneable 接口,通过实现这个接口的类都会具备clone 的能力,同时 clone 是在内存中进行,在性能方面会比我们直接通过 new 生成对象要高一些,特别是一些大的对象的生成,性能提升相对比较明显。那么在 Java 领域中,克隆分为深度克隆和浅克隆

 

**浅克隆**

​ 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

**实现一个邮件通知功能,告诉每个人今天晚上的上课时间,通过浅克隆
实现如下**

  email 实体类

 1 /**
 2  * @Description: email 实体类
 3  * @CreateDate: 2020/12/20 12:27
 4  * @UpdateDate: 2020/12/20 12:27
 5  * @Version: 1.0
 6  */
 7 public class Email implements Serializable {
 8 
 9 
10     private String content;
11 
12     public String getContent() {
13         return content;
14     }
15 
16     public void setContent(String content) {
17         this.content = content;
18     }
19 
20     @Override
21     public String toString() {
22         return "Email{" +
23                 "content='" + content + '\'' +
24                 '}';
25     }
26 
27 }
View Code

 

person实体类

 

/**
 * @Description: person实体类
 * @CreateDate: 2020/12/20 12:45
 * @UpdateDate: 2020/12/20 12:45
 * @UpdateRemark: 修改内容
 * @Version: 1.0
 */
public class Person implements  Cloneable {

    private String name;

    private Email email;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Email getEmail() {
        return email;
    }

    public void setEmail(Email email) {
        this.email = email;
    }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person)super.clone();
    }
}
View Code

 

深克隆

  被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍

 

public class CloneDemo {

    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {

        Email email=new Email();
        email.setContent("448341911@qq.com");
        Person p1=new Person("hah!");
        p1.setEmail(email);

//        Person p2 = p1.clone();
        Person p2 = p1.deepClone();
        p2.setName("黑白");
        p2.getEmail().setContent("licong@163.com");

        System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
        System.out.println(p2.getName()+"->"+p2.getEmail().getContent());

    }
}
View Code

 

这样就能实现深克隆效果,原理是把对象序列化输出到一个流中,然后在把对象从序列化流中读取出来,这个对象就不是原来的对象了。

常见的序列化技术

使用 JAVA 进行序列化有他的优点,也有他的缺点

优点:JAVA 语言本身提供,使用比较方便和简单

缺点:不支持跨语言处理、 性能相对不是很好,序列化以后产生的数据相对较大

XML 序列化框架

XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景,同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webservice,就是采用 XML 格式对数据进行序列化的

先引用maven依赖,引用xstream的包完成xml到javabean的映射

<dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.4.10</version>
</dependency>
View Code
public static void main(String[] args) {
        //java 序列化
        ISeralizer seralizer = new XmlSerializer();
        User user = new User();
        user.setName("zhangsan");
        user.setSex("男");
        user.setAge(20);
        user.setHobby("haha");
        User.num=10;
        byte[] serializer = seralizer.serializer(user);
        System.out.println(new String(serializer));
        User user1 = seralizer.deSerializer(serializer, User.class);
        System.out.println(user1+"->"+user1.sex);
    }
View Code

 

JSON 序列化框架

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于 XML 来说,JSON 的字节流更小,而且可读性也非常好。现在 JSON数据格式在企业运用是最普遍的

JSON 序列化常用的开源工具有很多

  1. Jackson (https://github.com/FasterXML/jackson

  2. 阿里开源的 FastJson (https://github.com/alibaba/fastjon

  3. Google 的 GSON (https://github.com/google/gson)

这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的

性能要好,但是 Jackson、GSON 的稳定性要比 Fastjson 好。而 fastjson 的优势在于提供的 api 非常容易使用

Hessian 序列化框架

Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说,Hessian 具有更好的性能和易用性,而且支持多种不同的语言

实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对Hessian 进行了重构,性能更高

 

Protobuf 序列化框架

Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。

Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件

Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中但是但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器

 

protobuf依赖

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.14.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.11</version>
</dependency>

 

编写user.proto 文件

syntax ="proto2";

package com.learn;

option java_package ="com.learn";
option java_outer_classname="UserProto";

message User{
    required string name=1;
    required int32 age=2;
}

 

proto 的语法

  1. 包名

  2. option 选项

  3. 消息模型(消息对象、字段(字段修饰符-required/optional/repeated)字段类型(基本数据类型、枚举、消息对象)、字段名、标识号)

生成实体类

在 protoc.exe 安装目录下执行如下命令

 

 

 

 

 

 

 

 .\protoc.exe --java_out=./ ./user.proto
/**
 * Protobuf 序列化框架
 *
 * Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。
 */
public class ProtobufDemo {

    public static void main(String[] args) {
        UserProto.User user = UserProto.User.newBuilder()
                .setName("haha")
                .setAge(18).build();

        System.out.println(user.toByteString());

    }
}

 

 

Protobuf 原理分析

核心原理: protobuf 使用 varint(zigzag)作为编码方式, 使用 T-L-V 作为存储方式

varint 编码方式

varint 是一种数据压缩算法,其核心思想是利用 bit 位来实现数据压缩。 比如:对于 int32 类型的数字,一般需要 4 个字节 表示;若采用 Varint 编码,对于很小的 int32 类型 数字,则可以用 1 个字节 假设我们定义了一个 int32 字段值=296.

第一步,转化为 2 进制编码

 

 

 

第二步,提取字节

规则: 按照从字节串末尾选取 7 位,并在最高位补 1,构成一个字节

 

 

 

第三步,截取第二个7位

整体右移 7 位,继续截取 7 个比特位,并且在最高位补 0 。因为这个是最后一个有意义的字节了。补 0 不影响结果

 

 

第四步,拼接成一个新的字节串

将原来用 4 个字节表示的整数,经过 varint 编码以后只需要 2 个字节了。

 

 

 

varint 编码对于小于 127 的数,可以最大化的压缩

varint 压缩小数据

比如我们压缩一个 var32 = 104 的数据

第一步,转换为 2 进制编码

 

 

第二步,提取字节

从末尾开始提取 7 个字节

并且在最高位最高位补 0,因为这个是最后的 7 位。

 

 

第三步,形成新的字节

 

 

也就是通过varint对于小于127以下的数字编码,只需要占用1个字节。

zigzag 编码方式

对于负数的处理,protobuf 使用 zigzag 的形式来存储。为什么负数需要用 zigzag 算法?

计算机语言中如何表示负整数?

在计算机中,定义了原码、反码和补码。来实现负数的表示。我们以一个字节 8 个 bit 来演示这几个概念数字 8 的二进制表示为 0000 1000

原码

通过第一个位表示符号(0 表示非负数、1 表示负数) ​ (+8) = {0000 1000} ​ (-8) = {1000 1000}

反码

因为第一位表示符号位,保持不变。剩下的位,非负数保持不变、负数按位取反。那对于上面的原码按照这个规则得到的结果(+8) = {0000 1000}原 ={0000 1000}反 非负数,剩下的位不变。所以和原码是保持一致(-8) = {1000 1000}原 ={1111 0111}反 负数,符号位不动,剩下为取反

但是通过原码和反码方式来表示二进制,还存在一些问题。

第一个问题:

0 这个数字,按照上面的反码计算,会存在两种表示 (+0) ={0000 0000}原= {0000 0000}反 (-0) ={1000 0000}原= {1111 1111}反

第二个问题:

符号位参与运算,会得到一个错误的结果,比如 1 + (-1)= {0000 0001}原 +{1 0000 0001}原 ={1000 0010}原 =-2 {0000 0001}反+ {1111 1110}反 = {1111 1111}反 =-0 不管是原码计算还是反码计算。得到的结果都是错误的。所以为了解决 这个问题,引入了补码的概念。

补码

补码的概念:第一位符号位保持不变,剩下的位非负数保持不变,负数按位取反且末位加 1

(+8) = {0000 1000}原 = {0000 1000}原 ={0000 1000}补 (-8) = {1000 1000}原 ={1111 0111}反={1111 1000}末位加一(补码) 8+(-8)= {0000 1000}补 +{1111 1000}末位加一(补码) ={0000 0000}=0

通过补码的方式,在进行符号运算的时候,计算机就不需要关心符号的问题,统一按照这个规则来计算。就没问题没问题

zigzag 原理

有了前面这块的基础以后,我们再来了解下 zigzag 的实现原理 比如我们存储一个 int32 = -2 按照上面提到的负数表现形式如下 原码{1 000 0010} ->取反 {1111 1101} ->整体加 1 {111 1110}->{1111 1110}

 

posted on 2021-01-17 15:47  窥视未来  阅读(170)  评论(0编辑  收藏  举报

导航