开源组件系列(5):数据的序列化(Thrift、Protobuf、Avro)

(一)数据序列化的意义

当我们需要将采集的日志存入文件或者是传输到下一个系统时,需要将数据对象转化为字节流格式,也就是数据的序列化过程。通常情况下,搭建一套数据仓库系统,会经历如下四个阶段:

1.没有序列化方案:该方案将数据定义为字符串格式,并以文本的形式保存,如果存在多条数据,则采用分隔符来分隔,例如“{|[”等。当增加字段时,通过在文本的最后增加新行的方式来进行,修改及解析比较繁琐,修改需要改动的依赖多。

2.采用编程语言内置的序列化方案,例如Java的Serialization等,这种方式会将日志系统与开发语言绑定起来。

3.格式化数据表达方式,如XML、JSON等,该方式能够约束每个字段的类型,为后续的优化和共享带来了便利。

4.采用序列化框架,例如Thrift、Avro、Protobuf等,通过引入schema的概念,使得序列化和维护变得非常高效,并且有了代码的特征。序列化框架的优点有:提供IDL用以描述数据的schema信息;支持跨语言的读写;数据的编码存储能够节约存储空间;支持schema的演化,也就是可以根据一定的规则修改schema。

目前比较常用的开源组建包括Facebook的Thrift、Google的Protobuf、Apache的Avro等,这些方案大同小异,并不存在绝对的优点,需要根据其特点灵活使用。

(二)Thrift方案

Thrift是Facebook开源的RPC框架,具备了序列化和RPC两个功能,几乎支持所有的编程语言,并提供了一套完整的IDL语言用来定义Schema信息,在实际应用中非常广泛。

以下是一个简单的Thrift例子:

// 命名空间定义

include "shared.thrift"

namespace cpp tutorial



// 定义别名

typedef i32 MyInteger

const i32 INT32CONSTANT = 9853



// 定义枚举类型

enum Operation {

  ADD = 1,

  SUBTRACT = 2,

  MULTIPLY = 3,

  DIVIDE = 4

}



// 定义结构体

struct Work {

  1: i32 num1 = 0,

  2: i32 num2,

  3: Operation op,

  4: optional string comment,

5: required int id

}

Thrift通过struct关键字来描述对象,由四种属性构成:

1.域编号:每个域必须是唯一的整数,可以不连续,通过该编号Thrift能够实现向前及向后的兼容性;

2.域修饰:包括required和optional两个关键字,用来对域的值进行限制;

3.域类型:包括int、long等基本类型,也支持set、list、map等复杂容器;

4.域名称:同一个struct下的域名必须唯一。

 

一旦给出了数据的Thrift IDL定以后,可以通过Thrift提供的编译器直接生成目标语言代码,例如Java:

thrift  --gen  java  tutorial.thrift

 

下面是Java使用例子:

public class HelloServerDemo {

    public  static  final int  SERVER_PORT = 8090;

    public void startServer() {

        try {

            System.out.println("HelloWorld TSimpleServer start ....");

            //在这里调用了 HelloWorldImpl 规定了接受的方法和返回的参数

            TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>( new HelloWorldImpl());



            TServerSocket serverTransport = new TServerSocket(SERVER_PORT);

            TServer.Args tArgs = new TServer.Args(serverTransport);

            tArgs.processor(tprocessor);

            tArgs.protocolFactory(new TBinaryProtocol.Factory());

            TServer server = new TSimpleServer(tArgs);

            server.serve();

        } catch (Exception e) {

            System.out.println("Server start error!!!");

            e.printStackTrace();

        }

    }



    public static void main(String[] args) {

        HelloServerDemo server = new HelloServerDemo();

        server.startServer();

    }

}



public class HelloClientDemo {



    public static final String SERVER_IP = "localhost";

    public static final int SERVER_PORT = 8090;

    public static final int TIMEOUT = 30000;



    public void startClient(String userName) {

        TTransport transport = null;

        try {

            transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);

            // 协议要和服务端一致

            TProtocol protocol = new TBinaryProtocol(transport);

            HelloWorldService.Client client = new HelloWorldService.Client(protocol);

            transport.open();

            String result = client.sayHello(userName);

            System.out.println("Thrify client result =: " + result);

        } catch (TTransportException e) {

            e.printStackTrace();

        } catch (TException e) {

            e.printStackTrace();

        } finally {

            if (null != transport) {

                transport.close();

            }

        }

    }



    public static void main(String[] args) {

        HelloClientDemo client = new HelloClientDemo();

        client.startClient("china");

    }

}

 

(三)Protobuf方案

Protobuf全名为Protocol Buffers,是Google开源的序列化框架,主要支持Java、C++及Python三种语言,语法和使用方式与Thrift非常类似,但没有实现RPC功能。由于采用了更加紧凑的数据编码方式,大部分情况下,Protobuf比Thrift占用的存储空间更小,且解析速度更快。如果不需要支持其他的语言,且准求性能,可以使用Protobuf来替代。

 

以下是一个简单的Protobuf例子:

syntax = "proto3";

package tutorial;



option java_package = "com.example.tutorial";

option java_outer_classname = "AddressBookProtos";



message Person {

    string name = 1;

    int32 id = 2;

    string email = 3;

}



message AddressBook {

    repeated Person people = 1;

    int32 id = 2;

}

Protobuf采用message关键字来统一描述对象,经过Protobuf编译后,会生成对应语言的类结构,同时,message也由四个属性构成:

1.域编号:每个域必须是唯一的整数,但可以不连续,该编号是前后兼容性的保障;

2.域修饰:包括required、optional和repeated三个关键字,repeated表示该域的值可以有多个;

3.域类型:支持常见的数据类型,包括bool、int32、float、double和string等,从Protobuf3开始,增加了对map的支持;

4.域名称:同一个message下每个域名必须唯一。

 

一旦给出了对应数据的IDL定以后,同样可以使用Protobuf提供的编译器生成目标语言代码,例如Java:

protoc  --java_out  src/  tutorial.proto

 

生成后的java代码被放在了当前目录src下。Java代码例子如下:

public class GenerareClass {

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

        String protoPath = System.getProperty("user.dir") + "\\src\\main\\resources\\proto";

        List<String> protoFileList = new ArrayList<String>();

        File f = new File(protoPath);

        File fa[] = f.listFiles();

        for (int i = 0; i < fa.length; i++) {

            File fs = fa[i];

            if (fs.isFile()) {

                protoFileList.add(fs.getName());

            }

        }

        for (String protoFile : protoFileList) {

            System.out.println(protoFile);

           

            String strCmd = "D:/file/blockChain/protobuf-java-3.5.1/protobuf-3.5.1/src/protoc.exe --java_out=../../java " + protoFile;

            Runtime.getRuntime().exec(strCmd, null, new File(protoPath));

        }

    }

}


 

(四)Avro方案

Avro是Hadoop生态中支持的RPC框架,是为了给Hadoop提供一个高效灵活且易于演化的基础库,具有如下几个特点:

1.动态类型:Avro不需要生成代码,它将数据与schema存放在一起,这样数据处理过程中不需要生成代码;

2.未标记数据:读取Avro数据时schema是已知的,因此类型信息减少,这使得序列化后的数据量变小;

3.不需要显示指定域编号:处理数据时新旧schema都是已知的,因此通过字段名称既可以解决兼容性问题。

 

以下是一个简单的Avro例子:

{"namespace": "tutorial.avro",

 "type": "record",

 "name": "User",

 "fields": [

     {"name": "name", "type": "string"},

     {"name": "favorite_number",  "type": ["int", "null"]},

     {"name": "favorite_color", "type": ["string", "null"]}

 ]
}

Avro的语法与前两种比较类似,只不过无需显示指定编号。Avro最初只支持JSON格式,后来增加了IDL的支持。Java语言的操作命令如下:

java  -jar  avro-tools.jar  idl  tutorial.avdl  tutorial.avpr

java  -jar  avro-tools.jar  compile  protocol  tutorial.avpr src/

 

Java例子如下:

User.Builder builder = User.newBuilder(); 

builder.setName("张三"); 

builder.setAge(30); 

builder.setEmail("zhangsan@*.com"); 

User user = builder.build(); 



//序列化 

File diskFile = new File("/data/users.avro"); 

DatumWriter<User> userDatumWriter = new SpecificDatumWriter<User>(User.class); 

DataFileWriter<User> dataFileWriter = new DataFileWriter<User>(userDatumWriter); 

//指定schema 

dataFileWriter.create(User.getClassSchema(), diskFile); 

dataFileWriter.append(user); 

dataFileWriter.fSync();//多次写入之后,可以调用fsync将数据同步写入磁盘(IO)通道 

user.setName("李四"); 

user.setEmail("lisi@*.com"); 

dataFileWriter.append(user); 

dataFileWriter.close(); 



//反序列化 

DatumReader<User> userDatumReader = new SpecificDatumReader<User>(User.class); 

// 也可以使用DataFileStream 

// DataFileStream<User> dataFileStream = new DataFileStream<User>(new FileInputStream(diskFile),userDatumReader); 

DataFileReader<User> dataFileReader = new DataFileReader<User>(diskFile, userDatumReader); 

User _current = null; 

while (dataFileReader.hasNext()) { 



    //注意:avro为了提升性能,_current对象只会被创建一次,且每次遍历都会重用此对象 

    //next方法只是给_current对象的各个属性赋值,而不是重新new。 

    _current = dataFileReader.next(_current); 

    //toString方法被重写,将获得JSON格式 

    System.out.println(_current); 

} 

dataFileReader.close();

 

(五)序列化框架的对比

 

 

原创文章 54 获赞 61 访问量 1万+

猜你喜欢

转载自blog.csdn.net/gaixiaoyang123/article/details/104485537
今日推荐