《Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理》第3章序列化与压缩,本章涉及了org.apache.hadoop.io包下最重要的两部分内容:序列化和压缩。本节为大家介绍序列化。
第3章 序列化与压缩
传统的计算机系统通过I/O操作与外界进行交流,Hadoop的I/O由传统的I/O系统发展而来,但又有些不同,Hadoop需要处理P、T级别的数据,所以在org.apache.hadoop.io包中包含了一些面向海量数据处理的基本输入输出工具,本章会对其中的序列化和压缩进行研究。
3.1 序列化
对象的序列化(Serialization)用于将对象编码成一个字节流,以及从字节流中重新构建对象。“将一个对象编码成一个字节流”称为序列化该对象(Serializing);相反的处理过程称为反序列化(Deserializing)。
序列化有三种主要的用途:
作为一种持久化格式:一个对象被序列化以后,它的编码可以被存储到磁盘上,供以后反序列化用。
作为一种通信数据格式:序列化结果可以从一个正在运行的虚拟机,通过网络被传递到另一个虚拟机上。
作为一种拷贝、克隆(clone)机制:将对象序列化到内存的缓存区中,然后通过反序列化,可以得到一个对已存对象进行深拷贝的新对象。
在分布式数据处理中,主要使用上面提到的前两种功能:数据持久化和通信数据格式。
在分析Hadoop的序列化机制前,先介绍一下Java内建的序列化机制。
3.1.1 Java内建序列化机制
Java序列化机制将对象转换为连续的byte数据,这些数据可以在日后还原为原先的对象状态,该机制还能自动处理不同操作系统上的差异,在Windows系统上序列化的Java对象,可以在UNIX系统上被重建出来,不需要担心不同机器上的数据表示方法,也不需要担心字节排列次序,如大端(big endian)、小端(little endian)或其他细节。
在Java中,使一个类的实例可被序列化非常简单,只需要在类声明中加入implements Serializable即可。Serializable接口是一个标志,不具有任何成员函数,其定义如下:
- public interface Serializable {
- }
Serializable接口没有任何方法,所以不需要对类进行修改,Block类通过声明它实现了Serializable接口,立即可以获得Java提供的序列化功能。代码如下:
- public class Block implements Writable, Comparable<Block>, Serializable
由于序列化主要应用在与I/O相关的一些操作上,其实现是通过一对输入/输出流来实现的。如果想对某个对象执行序列化动作,可以在某种OutputStream对象(后面还会讨论Java的流)的基础上创建一个对象流ObjectOutputStream对象,然后调用writeObject()就可达到目的。
writeObject()方法负责写入实现了Serializable接口对象的状态信息,输出数据将被送至该OutputStream。多个对象的序列化可以在ObjectOutputStream对象上多次调用writeObject(),分别写入这些对象。下面是序列化一个Block对象的例子:
- Block block1=new Block(7806259420524417791L, 39447755L, 56736651L);
- Block block2=new Block(5547099594945187683L, 67108864L, 56736828L);
- ……
- ByteArrayOutputStream out=new ByteArrayOutputStream();
- //在ByteArrayOutputStream的基础上创建ObjectOutputStream
- ObjectOutputStream objOut=new ObjectOutputStream(out);
- //对block进行序列化
- objOut.writeObject(block1);
对于Java基本类型的序列化,ObjectOutputStream提供了writeBoolean()、writeByte()等方法。
输入过程类似,将InputStream包装在ObjectInputStream中并调用readObject(),该方法返回一个指向向上转型后的Object的引用,通过向下转型,就可以得到正确结果。读取对象时,必须要小心地跟踪存储的对象的数量、顺序以及它们的类型。
Java的序列化机制非常“聪明”,JavaDoc中对ObjectOutputStream的writeObject()方法的说明是:“……这个对象的类、类签名、类的所有非暂态和非静态成员的值,以及它所有的父类都要被写入”,序列化机制会自动访问对象的父类,以保证对象内容的一致性。同时,序列化机制不仅存储对象在内存中的原始数据,还会追踪通过该对象可以到达的其他对象的内部数据,并描述所有这些对象是如何被链接起来的。对于复杂的情形,Java序列化机制也能应付自如:在输出objectA和objectB时,不会重复保存对象的序列化结果(如objectC,即objectC只被序列化一次);对于循环引用的对象,序列化也不会陷入死循环(如图3-1右图的情形)。
但是,序列化以后的对象在尺寸上有点过于充实了,以Block类为例,它只包含3个长整数,但是它的序列化结果竟然有112字节,而BlockMetaDataInfo其实只多了一个long型的成员变量,输出结果已经膨胀到190字节。包含3个长整数的Block对象的序列化结果如下:
- AC ED 00 05 73 72 00 1C 6F 72 67 2E 68 61 64 6F ....sr.. org.hado
- 6F 70 69 6E 74 65 72 6E 61 6C 2E 73 65 72 2E 42 opintern al.ser.B
- 6C 6F 63 6B E7 80 E3 D3 A6 B6 22 53 02 00 03 4A lock.... .."S...J
- 00 07 62 6C 6F 63 6B 49 64 4A 00 0F 67 65 6E 65 ..blockI dJ..gene
- 72 61 74 69 6F 6E 53 74 61 6D 70 4A 00 08 6E 75 rationSt ampJ..nu
- 6D 42 79 74 65 73 78 70 6C 55 67 95 68 E7 92 FF mBytesxp lUg.h...
- 00 00 00 00 03 61 BB 8B 00 00 00 00 02 59 EC CB .....a.. .....Y..
仔细看Block的输出会发现,序列化的结果中包含了大量与类相关的信息。Java的序列过程在《Java Object Serialization Specification》中规范,以Block为例,其结果的前两个字节是魔数(Magic Number)“AC ED”;后续两个字节是序列化格式的版本号,现在使用的版本号是5;接下来是类的描述信息,包括类的版本ID、是否实现writeObject()和readObject()方法等信息,对于拥有超类的类(如BlockMetaDataInfo),超类的信息也会递归地被保存下来;这些信息都写入OutputStream对象后,接下来才是对象的数据。在这个过程中,序列化输出中保存了大量的附加信息,导致序列化结果膨胀,对于需要保存和处理大规模数据的Hadoop来说,需要一个新的序列化机制。
由于篇幅的关系,不再详细讨论Java的序列化机制,有兴趣的读者可以参考《Java Object Serialization Specification》。
3.1.2 Hadoop序列化机制
和Java序列化机制不同(在对象流ObjectOutputStream对象上调用writeObject()方法),Hadoop的序列化机制通过调用对象的write()方法(它带有一个类型为DataOutput的参数),将对象序列化到流中。反序列化的过程也是类似,通过对象的readFields(),从流中读取数据。值得一提的是,Java序列化机制中,反序列化过程会不断地创建新的对象,但在Hadoop的序列化机制的反序列化过程中,用户可以复用对象:如,在Block的某个对象上反复调用readFields(),可以在同一个对象上得到多个反序列化的结果,而不是多个反序列化的结果对象(对象被复用了),这减少了Java对象的分配和回收,提高了应用的效率。
- public static void main(String[] args) {
- try {
- Block block1=new Block(7806259420524417791L, 39447755L, 56736651L);
- ……
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- DataOutputStream dout=new DataOutputStream(bout);
- block1.write(dout); //序列化对象到输出流dout中
- dout.close();
- System.out.println(……);
- SerializationExample.print16(out.toByteArray(), bout.size());
- }
- ……
- }
由于Block对象序列化时只输出了3个长整数, block1的序列化结果一共有24字节,如下所示。和Java的序列化机制的输出结果对比,Hadoop的序列化结果紧凑而且快速。
- AC ED 00 05 73 72 00 28 6F 72 67 2E 68 61 64 6F ....sr.( org.hado
- 6F 70 69 6E 74 65 72 6E opintern
3.1.3 Hadoop序列化机制的特征
对于处理大规模数据的Hadoop平台,其序列化机制需要具有如下特征:
紧凑:由于带宽是Hadoop集群中最稀缺的资源,一个紧凑的序列化机制可以充分利用数据中心的带宽。
快速:在进程间通信(包括MapReduce过程中涉及的数据交互)时会大量使用序列化机制,因此,必须尽量减少序列化和反序列化的开销。
可扩展:随着系统的发展,系统间通信的协议会升级,类的定义会发生变化,序列化机制需要支持这些升级和变化。
互操作:可以支持不同开发语言间的通信,如C++和Java间的通信。这样的通信,可以通过文件(需要精心设计文件的格式)或者后面介绍的IPC机制实现。
Java的序列化机制虽然强大,却不符合上面这些要求。Java Serialization将每个对象的类名写入输出流中,这导致了Java序列化对象需要占用比原对象更多的存储空间。同时,为了减少数据量,同一个类的对象的序列化结果只输出一份元数据,并通过某种形式的引用,来共享元数据。引用导致对序列化后的流进行处理的时候,需要保持一些状态。想象如下一种场景,在一个上百G的文件中,反序列化某个对象,需要访问文件中前面的某一个元数据,这将导致这个文件不能切割,并通过MapReduce来处理。同时,Java序列化会不断地创建新的对象,对于MapReduce应用来说,不能重用对象,在已有对象上进行反序列化操作,而是不断创建反序列化的各种类型记录,这会带来大量的系统开销。
3.1.4 Hadoop Writable机制
为了支持以上这些特性,Hadoop引入org.apache.hadoop.io.Writable接口,作为所有可序列化对象必须实现的接口,其类图如图3-2所示。
Writable机制紧凑、快速(但不容易扩展到Java以外的语言,如C、Python等)。和java.io.Serializable不同,Writable接口不是一个说明性接口,它包含两个方法:
- public interface Writable {
- /**
- * 输出(序列化)对象到流中
- * @param out DataOuput流,序列化的结果保存在流中
- * @throws IOException
- */
- void write(DataOutput out) throws IOException;
- /**
- * 从流中读取(反序列化)对象
- * 为了效率,请尽可能复用现有的对象
- * @param in DataInput流,从该流中读取数据
- * @throws IOException
- */
- void readFields(DataInput in) throws IOException;
- }
Writable.write()方法用于将对象状态写入二进制的DataOutput中,反序列化的过程由readFields()从DataInput流中读取状态完成。下面是一个例子:
- public class Block implements Writable, Comparable<Block>, Serializable {
- ……
- private long blockId;
- private long numBytes;
- private long generationStamp;
- ……
- public void write(DataOutput out) throws IOException {
- out.writeLong(blockId);
- out.writeLong(numBytes);
- out.writeLong(generationStamp);
- }
- public void readFields(DataInput in) throws IOException {
- this.blockId = in.readLong();
- this.numBytes = in.readLong();
- this.generationStamp = in.readLong();
- if (numBytes < 0) {
- throw new IOException("Unexpected block size: " + numBytes);
- }
- }
- ……
- }
这个例子使用的是前面分析Java序列化机制的Block类,Block实现了Writable接口,即需要实现write()方法和readFields()方法,这两个方法的实现都很简单:Block有三个成员变量,write()方法简单地把这三个变量写入流中,而readFields()则从流中依次读入这些数据,并做必要的检查。
Hadoop序列化机制中还包括另外几个重要接口:WritableComparable、RawComparator和WritableComparator。
WritableComparable,顾名思义,它提供类型比较的能力,这对MapReduce至关重要。该接口继承自Writable接口和Comparable接口,其中Comparable用于进行类型比较。ByteWritable、IntWritable、DoubleWritable等Java基本类型对应的Writable类型,都继承自WritableComparable。
效率在Hadoop中非常重要,因此HadoopI/O包中提供了具有高效比较能力的RawComparator接口。RawComparator和WritableComparable类图如图3-3所示。
RawComparator接口允许执行者比较流中读取的未被反序列化为对象的记录,从而省去了创建对象的所有开销。其中,compare()比较时需要的两个参数所对应的记录位于字节数组b1和b2的指定开始位置s1和s1,记录长度为l1和l2,代码如下:
- public interface RawComparator<T>extends
- Comparator<T> {
- public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
- }
以IntWritable为例,它的RawComparator实现中(WritableComparator是一个辅助类,实现了RawComparator接口),compare()方法通过readInt()直接在字节数组中读入需要比较的两个整数,然后输出Comparable接口要求的比较结果。值得注意的是,该过程中compare()方法避免使用IntWritable对象,从而避免了不必要的对象分配。相关代码如下:
- public static class Comparator extends WritableComparator {
- ……
- public int compare(byte[] b1, int s1, int l1,
- byte[] b2, int s2, int l2) {
- int thisValue = readInt(b1, s1);
- int thatValue = readInt(b2, s2);
- return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
- }
- ……
- }
WritableComparator是RawComparator对WritableComparable类的一个通用实现。它提供两个主要功能。首先,提供了一个RawComparator的compare()默认实现,该实现从数据流中反序列化要进行比较的对象,然后调用对象的compare()方法进行比较(这些对象都是Comparable的)。其次,它充当了RawComparator实例的一个工厂方法,例如,可以通过如下代码获得IntWritable的RawComparator:
- RawComparator<IntWritable>comparator=
- WritableComparator.get(IntWritable.class);
3.1.5 典型的Writable类详解(1)
Hadoop将很多Writable类归入org.apache.hadoop.io包中,类图如图3-4所示。
在这些类中,比较重要的有Java基本类、Text、Writable集合、ObjectWritable等,本节重点介绍Java基本类和ObjectWritable的实现。
1. Java基本类型的Writable封装
目前Java基本类型对应的Writable封装如表3-1所示。所有这些Writable类都继承自WritableComparable。也就是说,它们是可比较的。同时,它们都有get()和set()方法,用于获得和设置封装的值。
表3-1 Java基本类型对应的Writable封装
在表3-1中,对整型(int和long)进行编码的时候,有固定长度格式(IntWritable和LongWritable)和可变长度格式(VIntWritable和VLongWritable)两种选择。固定长度格式的整型,序列化后的数据是定长的,而可变长度格式则使用一种比较灵活的编码方式,对于数值比较小的整型,它们往往比较节省空间。同时,由于VIntWritable和VLongWritable的编码规则是一样的,所以VIntWritable的输出可以用VLongWritable读入。下面以VIntWritable为例,说明Writable的Java基本类封装实现。代码如下:
- public class VIntWritable implements WritableComparable {
- private int value;
- ……
- // 设置VIntWritable的值
- public void set(int value) { this.value = value; }
- // 获取VIntWritable的值
- public int get() { return value; }
- public void readFields(DataInput in) throws IOException {
- value = WritableUtils.read VInt(in);
- }
- public void write(DataOutput out) throws IOException {
- WritableUtils.writeVInt(out, value);
- }
- ……
- }
首先,每个Java基本类型的Writable封装,其类的内部都包含一个对应基本类型的成员变量value,get()和set()方法就是用来对该变量进行取值/赋值操作的。而Writable接口要求的readFields()和write()方法,VIntWritable则是通过调用Writable工具类中提供的readVInt()和writeVInt()读/写数据。方法readVInt()和writeVInt()的实现也只是简单调用了readVLong()和writeVLong(),所以,通过writeVInt()写的数据自然可以通过readVLong()读入。
writeVLong ()方法实现了对整型数值的变长编码,它的编码规则如下:
如果输入的整数大于或等于–112同时小于或等于127,那么编码需要1字节;否则,序列化结果的第一个字节,保存了输入整数的符号和后续编码的字节数。符号和后续字节数依据下面的编码规则(又一个规则):
如果是正数,则编码值范围落在–113和–120间(闭区间),后续字节数可以通过–(v+112)计算。
如果是负数,则编码值范围落在–121和–128间(闭区间),后续字节数可以通过–(v+120)计算。
3.1.5 典型的Writable类详解(2)
后续编码将高位在前,写入输入的整数(除去前面全0字节)。代码如下:
- public final class WritableUtils {
- public stati cvoid writeVInt(DataOutput stream, int i) throws IOException
- {
- writeVLong(stream, i);
- }
- /**
- * @param stream保存系列化结果输出流
- * @param i 被序列化的整数
- * @throws java.io.IOException
- */
- public static void writeVLong(DataOutput stream, long i) throws……
- {
- //处于[-112, 127]的整数
- if (i >= -112 && i <= 127) {
- stream.writeByte((byte)i);
- return;
- }
- //计算情况2的第一个字节
- int len = -112;
- if (i < 0) {
- i ^= -1L;
- len = -120;
- }
- long tmp = i;
- while (tmp != 0) {
- tmptmp = tmp >> 8;
- len--;
- }
- stream.writeByte((byte)len);
- len = (len < -120) ? -(len + 120) : -(len + 112);
- //输出后续字节
- for (int idx = len; idx != 0; idx--) {
- int shiftbits = (idx - 1) * 8;
- long mask = 0xFFL << shiftbits;
- stream.writeByte((byte)((i & mask) >> shiftbits));
- }
- }
- }
注意 本书附带代码对writeVLong()的输出结果做了一些分析,有兴趣的读者可以运行org.hadoopinternal.ser.VLongShow,分析一些典型整数的writeVLong()输出结果。
2. ObjectWritable类的实现
针对Java基本类型、字符串、枚举、Writable、空值、Writable的其他子类,ObjectWritable提供了一个封装,适用于字段需要使用多种类型。ObjectWritable可应用于Hadoop远程过程调用(将在第4章介绍)中参数的序列化和反序列化;ObjectWritable的另一个典型应用是在需要序列化不同类型的对象到某一个字段,如在一个SequenceFile的值中保存不同类型的对象(如LongWritable值或Text值)时,可以将该值声明为ObjectWritable。
ObjectWritable的实现比较冗长,需要根据可能被封装在ObjectWritable中的各种对象进行不同的处理。ObjectWritable有三个成员变量,包括被封装的对象实例instance、该对象运行时类的Class对象和Configuration对象。
ObjectWritable的write方法调用的是静态方法ObjectWritable.writeObject(),该方法可以往DataOutput接口中写入各种Java对象。
writeObject()方法先输出对象的类名(通过对象对应的Class 对象的getName()方法获得),然后根据传入对象的类型,分情况系列化对象到输出流中,也就是说,对象通过该方法输出对象的类名,对象序列化结果对到输出流中。在ObjectWritable.writeObject()的逻辑中,需要分别处理null、Java数组、字符串String、Java基本类型、枚举和Writable的子类6种情况,由于类的继承,处理Writable时,序列化的结果包含对象类名,对象实际类名和对象序列化结果三部分。
3.1.5 典型的Writable类详解(3)
为什么需要对象实际类名呢?根据Java的单根继承规则,ObjectWritable中传入的declaredClass,可以是传入instance对象对应的类的类对象,也可以是instance对象的父类的类对象。但是,在序列化和反序列化的时候,往往不能使用父类的序列化方法(如write方法)来序列化子类对象,所以,在序列化结果中必须记住对象实际类名。相关代码如下:
- public class ObjectWritable implements Writable, Configurable {
- private Class declaredClass;//保存于ObjectWritable的对象对应的类对象
- private Object instance;//被保留的对象
- private Configuration conf;
- public ObjectWritable() {}
- public ObjectWritable(Object instance) {
- set(instance);
- }
- public ObjectWritable(Class declaredClass, Object instance) {
- this.declaredClass = declaredClass;
- this.instance = instance;
- }
- ……
- public void readFields(DataInput in) throws IOException {
- readObject(in, this, this.conf);
- }
- public void write(DataOutput out) throws IOException {
- writeObject(out, instance, declaredClass, conf);
- }
- ……
- public static void writeObject(DataOutput out, Object instance,
- Class declaredClass,Configuration conf) throws……{
- if (instance == null) {//空
- instance = new NullInstance(declaredClass, conf);
- declaredClass = Writable.class;
- }
- // 写出declaredClass的规范名
- UTF8.writeString(out, declaredClass.getName());
- if (declaredClass.isArray()) {//数组
- ……
- } else if (declaredClass == String.class) {//字符串
- ……
- } else if (declaredClass.isPrimitive()) {//基本类型
- if (declaredClass == Boolean.TYPE) { //boolean
- out.writeBoolean(((Boolean)instance).booleanValue());
- } else if (declaredClass == Character.TYPE) { //char
- ……
- }
- } else if (declaredClass.isEnum()) {//枚举类型
- ……
- } else if (Writable.class.isAssignableFrom(declaredClass)) {
- //Writable的子类
- UTF8.writeString(out, instance.getClass().getName());
- ((Writable)instance).write(out);
- } else {
- ……
- }
- public static Object readObject(DataInput in,
- ObjectWritable objectWritable, Configuration conf){
- ……
- Class instanceClass = null;
- ……
- Writable writable = WritableFactories.newInstance(instanceClass,
- conf);
- writable.readFields(in);
- instance = writable;
- ……
- }
- }
和输出对应,ObjectWritable的readFields()方法调用的是静态方法ObjectWritable.readObject (),该方法的实现和writeObject()类似,唯一值得研究的是Writable对象处理部分,readObject ()方法依赖于WritableFactories类。WritableFactories类允许非公有的Writable子类定义一个对象工厂,由该工厂创建Writable对象,如在上面的readObject ()代码中,通过WritableFactories的静态方法newInstance(),可以创建类型为instanceClass的Writable子对象。相关代码如下:
- public class WritableFactories {
- //保存了类型和WritableFactory工厂的对应关系
- private static final HashMap<Class, WritableFactory>CLASS_TO_FACTORY
- = new HashMap<Class, WritableFactory>();
- ……
- public static Writable newInstance(Class<? extends Writable> c,
- Configuration conf) {
- WritableFactory factory = WritableFactories.getFactory(c);
- if (factory != null) {
- Writable result = factory.newInstance();
- if (result instanceof Configurable) {
- ((Configurable) result).setConf(conf);
- }
- return result;
- } else {
- //采用传统的反射工具ReflectionUtils,创建对象
- return ReflectionUtils.newInstance(c, conf);
- }
- }
- }
3.1.5 典型的Writable类详解(4)
WritableFactories.newInstance()方法根据输入的类型查找对应的WritableFactory工厂对象,然后调用该对象的newInstance()创建对象,如果该对象是可配置的,newInstance()还会通过对象的setConf()方法配置对象。
WritableFactories提供注册机制,使得这些Writable子类可以将该工厂登记到WritableFactories的静态成员变量CLASS_TO_FACTORY中。下面是一个典型的WritableFactory工厂实现,来自于HDFS的数据块Block。其中,WritableFactories.setFactory()需要两个参数,分别是注册类对应的类对象和能够构造注册类的WritableFactory接口的实现,在下面的代码里,WritableFactory的实现是一个匿名类,其newInstance()方法会创建一个新的Block对象。
- public class Block implements Writable, Comparable<Block> {
- static {
- WritableFactories.setFactory
- (Block.class,//类对象
- new WritableFactory() {//对应类的WritableFactory实现
- public Writable newInstance() { return new Block(); }
- });
- }
- ……
- }
ObjectWritable作为一种通用机制,相当浪费资源,它需要为每一个输出写入封装类型的名字。如果类型的数量不是很多,而且可以事先知道,则可以使用一个静态类型数组来提高效率,并使用数组索引作为类型的序列化引用。GenericWritable就是因为这个目的被引入org.apache.hadoop.io包中,由于篇幅关系,不再详细介绍,有兴趣的读者可以继续分析GenericWritable的源代码。
3.1.6 Hadoop序列化框架
大部分的MapReduce程序都使用Writable键–值对作为输入和输出,但这并不是Hadoop的API指定的,其他序列化机制也能和Hadoop配合,并应用于MapReduce中。
目前,除了前面介绍过的Java序列化机制和Hadoop使用的Writable机制,还流行其他序列化框架,如Hadoop Avro、Apache Thrift和Google Protocol Buffer。
Avro是一个数据序列化系统,用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷、快速地处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便地处理 Avro数据。
Thrift是一个可伸缩的、跨语言的服务开发框架,由Facebook贡献给开源社区,是Facebook的核心框架之一。基于Thrift的跨平台能力封装的Hadoop文件系统Thrift API(参考contrib的thriftfs模块),提供了不同开发语言开发的系统访问HDFS的能力。
Google Protocol Buffer是Google内部的混合语言数据标准,提供了一种轻便高效的结构化数据存储格式。目前,Protocol Buffers提供了C++、Java、Python三种语言的API,广泛应用于Google内部的通信协议、数据存储等领域中。
Hadoop提供了一个简单的序列化框架API,用于集成各种序列化实现,该框架由Serialization实现(在org.apache.hadoop.io.serializer包中)。
Serialization是一个接口,使用抽象工厂的设计模式,提供了一系列和序列化相关并相互依赖对象的接口。通过Serialization应用可以获得类型的Serializer实例,即将一个对象转换为一个字节流的实现实例;Deserializer实例和Serializer实例相反,它用于将字节流转为一个对象。很明显,Serializer和Deserializer相互依赖,所以必须通过抽象工厂Serialization,才能获得对应的实现。相关代码如下:
- public interface Serialization<T> {
- //客户端用于判断序列化实现是否支持该类对象
- boolean accept(Class<?> c);
- //获得用于序列化对象的Serializer实现
- Serializer<T> getSerializer(Class<T> c);
- //获得用于反序列化对象的Deserializer实现
- Deserializer<T> getDeserializer(Class<T> c);
- }
如果需要使用Serializer来执行序列化,一般需要通过open()方法打开Serializer,open()方法传入一个底层的流对象,然后就可以使用serialize()方法序列化对象到底层的流中。最后序列化结束时,通过close()方法关闭Serializer。Serializer接口的相关代码如下:
- public interface Serializer<T> {
- //为输出(序列化)对象做准备
- void open(OutputStream out) throws IOException;
- //将对象序列化到底层的流中
- void serialize(T t) throws IOException;
- //序列化结束,清理
- void close() throws IOException;
- }
Hadoop目前支持两个Serialization实现,分别是支持Writable机制的WritableSerialization和支持Java序列化的JavaSerialization。通过JavaSerialization可以在MapReduce程序中方便地使用标准的Java类型,如int或String,但如同前面所分析的,Java的Object Serialization不如Hadoop的序列化机制有效,非特殊情况不要轻易尝试。