一、概述
1、前言
- 常见的序列化协议:
- JSON
- XML
- Hessian
- Thrift
- ProtoBuf 的优点:
-
Protobuf解析速度快(即序列化反序列化速度快),
-
占用空间小,以及兼容性好,很适合做数据存储或网络通讯间的数据传输。
-
2、JDK原生序列化
package protobuf;
public class Teacher implements Serializable {
private long teacherId;
private String name;
private int age;
private List<String> courses = new ArrayList<>();
public Teacher(long teacherId, String name, int age) {
this.teacherId = teacherId;
this.name = name;
this.age = age;
}
// getter and setter...
@Override
public String toString() {
return "Teacher{" +
"teacherId=" + teacherId +
", name='" + name + ''' +
", age=" + age +
", courses=" + courses +
'}';
}
}
public class Test_JDK {
public static void main(String[] args) throws Exception {
Teacher tim = new Teacher(1L, "Tim", 34);
tim.setCourses(new ArrayList<>(Arrays.asList("aaaa", "aaaa")));
// 序列化
byte[] byteArray = serialize(tim);
System.out.println(Arrays.toString(byteArray));
// 反序列化
Teacher teacher = deserialize(byteArray);
System.out.println(teacher);
}
// 序列化
private static byte[] serialize(Teacher tim) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(tim);
return bos.toByteArray();
}
// 反序列化
private static Teacher deserialize(byte[] bytes) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
return (Teacher) ois.readObject();
}
}
- 输出
[-84, -19, 0, 5, 115, 114, 0, 16, 112, 114, 111, 116, 111, 98, 117, 102, 46, 84, 101, 97, 99,
104, 101, 114, -109, 117, -76, 44, 106, 50, -50, -61, 2, 0, 4, 73, 0, 3, 97, 103, 101, 74, 0,
9, 116, 101, 97, 99, 104, 101, 114, 73, 100, 76, 0, 7, 99, 111, 117, 114, 115, 101, 115, 116,
0, 16, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 76, 0, 4, 110,
97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105,
110, 103, 59, 120, 112, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 1, 115, 114, 0, 19, 106, 97, 118, 97,
46, 117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103,
-57, 97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 2, 119, 4, 0, 0, 0, 2,
116, 0, 4, 97, 97, 97, 97, 113, 0, 126, 0, 6, 120, 116, 0, 3, 84, 105, 109]
Teacher{teacherId=1, name='Tim', age=34, courses=[aaaa, aaaa]}
3、通过Protobuf 序列化
- 首先下载:
https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0/protobuf-java-3.7.0.zip
https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0/protoc-3.7.0-win64.zip
- 需要定义一个teacher.proto
syntax = "proto2";
option java_package = "edu.xpu";
option java_outer_classname = "TeacherSerializer";
message Teacher{
required int64 teacherId = 1;
required int32 age = 2;
required string name = 3;
repeated string courses = 4;
}
message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
- 当前生成的Java文件中用到的类还需要我们引入Protobuf的依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.13.0</version>
</dependency>
- 把生成的Java文件给复制到工程当中,然后测试一下序列化和反序列化
import java.util.Arrays;
public class ProtobufTest {
public static void main(String[] args) throws Exception {
byte[] bytes = serialize();
System.out.println(Arrays.toString(bytes));
TeacherSerializer.Teacher teacher = deserialize(bytes);
System.out.println(teacher);
}
// 序列化
private static byte[] serialize(){
// 构造器,构造Teacher
TeacherSerializer.Teacher.Builder builder = TeacherSerializer.Teacher.newBuilder();
builder.setName("Tim")
.setAge(34)
.setTeacherId(1L)
.addCourses("Java");
TeacherSerializer.Teacher teacher = builder.build();
return teacher.toByteArray();
}
// 反序列化
private static TeacherSerializer.Teacher deserialize(byte[] bytes) throws Exception {
return TeacherSerializer.Teacher.parseFrom(bytes);
}
}
- 同样属性的JavaBean对象,但是通过Protobuf序列化和反序列化的代价却小很多,和Java原生序列化的大小产生了鲜明对比:
二、Protobuf特性与基本原理
1、将字段长度减小
- 对于一条信息,
json
的表示方式为:
{ "age": 30, "name": "zhangsan", "height": 175.33, "weight": 140 }
- 存在大量的冗余数据
- 如果这样子存储,就可以减少大量的数据
2、进一步压缩
- 假设
height
这个字段为null
,我们其实是不必要传递这个字段的,这个时候我们需要传递的数据就为:
-
tag 技术
-
每个字段我们都用
tag|value
的方式来存储的,在tag
当中记录两种信息,一个是value
对应的字段的编号,另一个是value
的数据类型(比如是整形还是字符串等),因为tag
中有字段编号信息,所以即使没有传递height
字段的value
值,根据编号也能正确的配对。 -
传统的 json 序列化中, tag 存储的是 字符串
-
protobuf 序列化, tag 存储的是二进制编号,一般只会占据一个字节
3、protobuf支持的字段类型
因为tag
一般占用一个字节,开销还算是比较小的,所以protobuf
整体的存储空间占用还是相对小了很多的。
4、进一步压缩
-
在实际的传输过程中,会传递整数,我们知道整数在计算机当中占据4个字节,但是绝大部分的整数,比如价格,库存等,都是比较小的整数,实际用不了4个字节,像127这种数,在计算机中的二进制是:
00000000 00000000 00000000 01111111
(4字节32位) -
完全可以用最后1个字节来进行存储,
protobuf
当中定义了Varint
这种数据类型,可以以不同的长度来存储整数,将数据进一步的进行了压缩。 -
但是这里面也有一个问题,在计算机当中的负数是用补码表示的,对于-1,它的二进制表示方式为:
11111111 11111111 11111111 11111111
(4字节32位)显然无法用1个字节来表示了, -
但-1确实是一个比较简单的数,这个时候就可以使用算法来对负数进行进一步的压缩,最终我们可以使用2个字节来表示-1。
5、 要快
- 虽然数据现在很小了,但是解析速度还是有很大的提升空间的,因为每个字段都是用
tag|value
来表示的,在tag
中含有value
的数据类型的信息,而不同的数据类型有不同的大小,比如如果value
是bool
型,我们就知道肯定占了一个字节,程序从tag
后面直接读一个字节就可以解析出value
,非常快,而json
则需要进行字符串解析才可以办到。