【Hadoop学习】Hadoop的序列化

对象的序列化用于将对象编码成一个字节流,以及从字节流中重新构建对象。
将一个对象编码成一个字节流叫做序列化改对象(Serializing)
相反的处理过程称为反序列化(Deserializing)

序列化的主要用途:

  1. 作为一种持久化格式
    一个对象被序列化之后,它的编码可以被存储到磁盘上,供反序列化使用
  2. 作为一种通信数据格式
    序列化的结果可以从一个正在运行的虚拟机,通过网络传输到一个虚拟机上
  3. 作为一种拷贝,克隆机制
    将对象序列化到内存,然后通过反序列化,可以得到一个对已经存在的对象的深拷贝的新对象

在hadoop中,序列化是要使用的是数据持久化和通信数据格式两种功能。

JDK中自带序列化

JDK的序列化只有实现了serializable接口就能实现序列化与反序列化,但是记得一定要加上序列化版本ID serialVersionUID,这个是用来识别序列化的之前那个类的到底是哪一个。
我们显式设置这个序列化版本ID的目的就是为了:

  1. 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
  2. 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

java序列化算法要点:

  • 将对象实例相关的元数据输出
  • 递归的输出类的超类描述直到不再有超类
  • 类元数据完了之后,开始从最顶层的超类开始输出对象的实例的实际数据值
  • 从上到下递归输出实例的数据

java序列化与反序列化实现方法

  1. 创建一个对象,并实现Serializbale接口
  2. 序列化ObjectOutputStream.writeObject(obj);
    反序列化ObjectOutputStream.readObject();

java序列化不足之处:
java序列化将每个对象的类名写入到输出流中,这就导致了java序列化对象需要占用比原对象更多的存储空间。
java的反序列化会不断的创建对象,这会给系统带来一定的开销。

正是由于java序列化的不足,所以hadoop没有直接使用java序列化,而是实现了自己的序列化机制,在hadoop序列化中,用户可以复用对象,这样就减少了java对象的分配和回收,提高了应用的效率。

Hadoop序列化特点:

  • 紧凑
    由于带宽是hadoop集群中稀缺的资源,一个紧凑的序列化机制可以充分利用数据中心的带宽
  • 快速
    在进程间通信时会大量使用序列化机制,因此必须尽量减少序列化和反序列化的开销
  • 可扩展
    随着系统的发展,系统间通信的协议升级,类的定义会发生变化,序列化机制需要支持这些升级和变化
  • 互操作
    可以支持不同开发语言间的通信,如c++和java之间的通信。

Hadoop序列化

hadoop中通过实现Writable接口来实现序列化,它比较紧凑、快速。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Writable {
/**
* 输出(序列化)对象到流中
* @param out
* @throws IOException
*/

void write(DataOutput out) throws IOException;

/**
* 从流中读取(反序列化对象)
* @param in
* @throws IOException
*/

void readFields(DataInput in) throws IOException;
}

write方法用于将对象写入二进制的DataOutput中,完成序列化
readFields从DataInput流中读取数据,完成反序列化

下面是一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyWritable implements Writable {
// Some data
private int counter;

private long timestamp;

public void write(DataOutput out) throws IOException {
out.writeInt(counter);
out.writeLong(timestamp);
}

public void readFields(DataInput in) throws IOException {
counter = in.readInt();
timestamp = in.readLong();
}

public static MyWritable read(DataInput in) throws IOException {
MyWritable w = new MyWritable();
w.readFields(in);
return w;
}
}

hadoop中常用的序列化文件如下图所示
hadoop序列化

BytesWritable

BytesWritable类型是一个二进制数组的封装类型,序列化格式是以一个4字节的整数。

NullWritable

NullWritable是一个非常特殊的Writable类型,序列化不包含任何字符,仅仅相当于个占位符。在MapReduce编程中,key或者value在不需要使用时,可以定义为NullWritable。

ObjectWritable

ObjectWritable是其他类型的封装类,包括java原生类型,String,enum,null等,或者这些类型的数组。当一个field有多种类型时,就可以使用ObjectWritable,不过有个不好的地方就是占用的空间太大,即使你存一个字母,因为它需要保存封装前的类型。

GenericWritable

使用GenericWritable时,只需继承他,并通过getTypes方法指定哪些类型需要支持即可。
GenericWritable的序列化只是把类型在type数组里的索引放在了前面,这样就比ObjectWritable节省了很多空间,所以推荐大家使用GenericWritable

集合类型的Writable

ArrayWritable和TwoDArrayWritable

ArrayWritable和TwoDArrayWritable分别表示数组和二维数组的Writable类型,指定数组的类型有两种方法,构造方法里设置,或者继承于ArrayWritable,TwoDArrayWritable也是一样。

MapWritable和SortedMapWritable

MapWritable对应Map,SortedMapWritable对应SortedMap,以4个字节开头,存储集合大小,然后每个元素以一个字节开头存储类型的索引(类似GenericWritable,所以总共的类型总数只能倒127),接着是元素本身,先key后value,这样一对对排开。
这两个Writable以后会用很多,贯穿整个hadoop,这里就不写示例了。

hadoop序列化中没有set集合和list集合,但是可以代替实现。用MapWritable代替set,SortedMapWritable代替sortedmap,只需将他们的values设置成NullWritable即可,NullWritable不占空间。可以用ArrayWritable代替list集合,不同类型的list可以用GenericWritable实现类型,然后再使用ArrayWritable封装。当然MapWritable一样可以实现list,把key设置为索引,values做list里的元素。