数据schemaAvro简介

简介:

文章结束给大家来个程序员笑话:[M]

    最近在研究Thrift和Avro以及它们的区分,通过各种渠道搜集资料,现整顿出有关Avro的一些资料,方便当前参考。

    一、弁言

    1、 简介

    Avro是Hadoop中的一个子项目,也是Apache中一个独立的项目,Avro是一个基于二进制数据传输高性能的旁边件。在Hadoop的其他项目中例如HBase(Ref)和Hive(Ref)的Client端与服务端的数据传输也采取了这个工具。Avro是一个数据序列化的系统。Avro可以将数据结构或对象转化成便于存储或传输的格式。Avro设计之初就用来支撑数据密集型应用,适合于近程或当地大规模数据的存储和交换。

    2、 特色

    Ø  丰硕的数据结构类型;

    Ø  快速可压缩的二进制数据形式,对数据二进制序列化后可以节约数据存储空间和网络传输带宽;

    Ø  存储持久数据的文件容器;

    Ø  可以实现近程进程调用RPC;

    Ø  简略的动态语言结合功能。

    avro支撑跨编程语言实现(C, C++, C#,Java, Python, Ruby, PHP),相似于Thrift,但是avro的明显特征是:avro依附于模式,动态加载相关数据的模式,Avro数据的读写操纵很频繁,而这些操纵使用的都是模式,这样就减少写入每个数据文件的开销,使得序列化快速而又轻巧。这种数据及其模式的自我描述方便了动态脚本语言的使用。当Avro数据存储到文件中时,它的模式也随之存储,这样任何程序都可以对文件停止处置。如果读取数据时使用的模式与写入数据时使用的模式不同,也很轻易解决,因为读取和写入的模式都是已知的。

    

New schema

Writer

Reader

Action

Added field

Old

New

The reader uses the default value of the new field, since it is not written by the writer.

 

New

Old

The reader does not know about the new field written by the writer, so it is ignored

(projection).

Removed field

Old

New

The reader ignores the removed field (projection).

 

New

Old

The removed field is not written by the writer. If the old schema had a default defined

for the field, the reader uses this; otherwise, it gets an error. In this case, it is best to

update the reader’s schema, either at the same time as or before the writer’s.

    Avro和动态语言结合后,读/写数据文件和使用RPC协议都不需要生成代码,而代码生成作为一种可选的优化只需要在静态类型语言中实现。

    Avro依附于模式(Schema)。通过模式定义各种数据结构,只有确定了模式才能对数据停止解释,所以在数据的序列化和反序列化之前,必须先确定模式的结构。恰是模式的引入,使得数据具有了自描述的功能,同时能够实现动态加载,另外与其他的数据序列化系统如Thrift相比,数据之间不存在其他的任何标识,有利于进步数据处置的效率。    

    二、技巧要领

    1、 类型

    数据类型标准化的意思:一方面使不同系统对相同的数据能够准确剖析,另一方面,数据类型的标准定义有利于数据序列化/反序列化。

    简略的数据类型:Avro定义了几种简略数据类型,下表是其简略说明:

    

类型

说明

null

no value

boolean

a binary value

int

32-bit signed integer

long

64-bit signed integer

float

single precision (32-bit) IEEE 754 floating-point number

double

double precision (64-bit) IEEE 754 floating-point number

bytes

sequence of 8-bit unsigned bytes

string

unicode character sequence

    简略数据类型由类型名称定义,不包含属性信息,例如字符串定义如下:

    {"type": "string"}

    复杂数据类型:Avro定义了六种复杂数据类型,每一种复杂数据类型都具有独特的属性,下表就每一种复杂数据类型停止说明。

    

类型

属性

说明

Records

type name

record

name

a JSON string providing the name of the record (required).

namespace

a JSON string that qualifies the name(optional).

doc

a JSON string providing documentation to the user of this schema (optional).

aliases

a JSON array of strings, providing alternate names for this record (optional).

fields

a JSON array, listing fields (required).

            name

a JSON string.

            type

a schema/a string of defined record.

            default

a default value for field when lack.

            order

ordering of this field.

Enums

type name

enum

 

name

a JSON string providing the name of the enum (required).

namespace

a JSON string that qualifies the name.

doc

a JSON string providing documentation to the user of this schema (optional).

aliases

a JSON array of strings, providing alternate names for this enum (optional)

symbols

a JSON array, listing symbols, as JSON strings (required). All symbols in an enum must be unique.

Arrays

type name

array

 

items

the schema of the array’s items.

Maps

type name

map

 

values

the schema of the map’s values.

Fixed

type name

fixed

 

name

a string naming this fixed (required).

 

namespace

a string that qualifies the name.

 

aliases

a JSON array of strings, providing alternate names for this enum (optional).

 

size

an integer, specifying the number of bytes per value (required).

Unions

 

a JSON arrays

    每一种复杂数据类型都含有各自的一些属性,其中部分属性是必需的,部分是可选的。

    这里需要说明Record类型中field属性的默认值,当Record Schema实例数据中某个field属性没有供给实例数据时,则由默认值供给,详细值见下表。Union的field默认值由Union定义中的第一个Schema决议。

    

avro type

json type

example

null

null

null

boolean

boolean

true

int,long

integer

1

float,double

number

1.1

bytes

string

"\u00FF"

string

string

"foo"

record

object

{"a": 1}

enum

string

"FOO"

array

array

[1]

map

object

{"a": 1}

fixed

string

"\u00ff"

    2、 序列化/反序列化

    Avro指定两种数据序列化编码方式:binary encoding 和Json encoding。使用二进制编码会高效序列化,并且序列化后失掉的结果会比拟小;而JSON一般用于调试系统或是基于WEB的应用。

    binary encoding规矩如下:

    1、  简略数据类型

    

Type

Encoding

Example

null

Zero bytes

Null

boolean

A single byte

{true:1, false:0}

int/long

variable-length zig-zag coding

 

float

4 bytes

Java's floatToIntBits

double

8 bytes

Java's doubleToLongBits

bytes

a long followed by that many bytes of data

 

string

a long followed by that many bytes of UTF-8 encoded character data

“foo”:{3,f,o,o}

06 66 6f 6f

    2、  复杂数据类型

    

Type

encoding

Records

encoded just the concatenation of the encodings of its fields

Enums

a int representing the zero-based position of the symbol in the schema

Arrays

encoded as series of blocks. A block with count 0 indicates the end of the array. block:{long,items}

Maps

encoded as series of blocks. A block with count 0 indicates the end of the map. block:{long,key/value pairs}.

Unions

encoded by first writing a long value indicating the zero-based position within the union of the schema of its value. The value is then encoded per the indicated schema within the union.

fixed

encoded using number of bytes declared in the schema

    实例:

    Ø records

    {

    "type":"record",

    "name":"test",

    "fields" : [

    {"name": "a","type": "long"},

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

    ]

    }

    假设:a=27b=”foo” (encoding:36(27), 06(3), 66("f"), 6f("o"))

    binary encoding:3606 66 6f 6f

    Ø enums

    {"type": "enum","name": "Foo", "symbols": ["A","B", "C", "D"] }

    “D”(encoding: 06(3))

    binary encoding: 06

    Ø arrays

    {"type": "array","items": "long"}

    每日一道理 
我把卷子摊在课桌上,恨不得敲一阵锣,叫大家都来看看我这光彩的分数。

    设:{3, 27 } (encoding:04(2), 06(3), 36(27) )

    binary encoding:0406 36 00

    Ø maps

    设:{("a":1), ("b":2) } (encoding:61(“a”), 62(“b”), 02(1), 04(2))

    binary encoding:0261 02 02 62 04

    Ø unions

    ["string","null"]

    设:(1)null; (2) “a”

    binary encoding:

    (1) 02;说明:02代表null在union定义中的位置1;

    (2) 00 02 61;说明:00为string在union定义的位置,02 61为”a”的编码。

 

    图1表现的是Avro当地序列化和反序列化的实例,它将用户定义的模式和详细的数据编码成二进制序列存储在对象容器文件中,例如用户定义了包含学号、姓名、院系和电话的先生模式,而Avro对其停止编码后存储在student.db文件中,其中存储数据的模式放在文件头的元数据中,这样读取的模式即使与写入的模式不同,也可以迅速地读出数据。假如另一个程序需要获取先生的姓名和电话,只需要定义包含姓名和电话的先生模式,然后用此模式去读取容器文件中的数据便可。

    数据和schema

                                                          图表 1

 

    3、 模式Schema

    Schema通过JSON对象表现。Schema定义了简略数据类型和复杂数据类型,其中复杂数据类型包含不同属性。通过各种数据类型用户可以自定义丰硕的数据结构。

    Schema由下列JSON对象之一定义:

    1. JSON字符串:定名

    2. JSON对象:{“type”: “typeName” …attributes…}

    3. JSON数组:Avro中Union的定义

    举例:

    {"namespace": "example.avro",

 "type":"record",

 "name":"User",

 "fields": [

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

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

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

 ]

    }

    4、 排序

    Avro为数据定义了一个标准的排列顺序。比拟在很多时候是经常被使用到的对象之间的操纵,标准定义可以停止方便有效的比拟和排序。同时标准的定义可以方便对Avro的二进制编码数据直接停止排序而不需要反序列化。

    只有当数据项包含相同的Schema的时候,数据之间的比拟才有意思。数据的比拟按照Schema深度优先,从左至右的顺序递归的停止。找到第一个不匹配便可终止比拟。

    两个具有相同的模式的项的比拟按照以下规矩停止:

       null:总是相等。

    int,long,float:按照数值大小比拟。

    boolean:false在true之前。

    string:按照字典序停止比拟。

    bytes,fixed:按照byte的字典序停止比拟。

    array:按照元素的字典序停止比拟。

    enum:按照符号在枚举中的位置比拟。

    record:按照域的字典序排序,如果指定了以下属性:

    “ascending”,域值的顺序稳定。

    “descending”,域值的顺序倒置。

    “ignore”,排序的时候忽略域值。

    map:不可停止比拟。

 

    5、 对象容器文件

    Avro定义了一个简略的对象容器文件格式。一个文件对应一个模式,所有存储在文件中的对象都是根据模式写入的。对象按照块停止存储,块可以采取压缩的方式存储。为了在停止mapreduce处置的时候有效的切分文件,在块之间采取了同步记号。一个文件可以包含恣意用户定义的元数据。

    一个文件由两部分组成:文件头和一个或者多个文件数据块。

    文件头:

    Ø 四个字节,ASCII‘O’,‘b’,‘j’,1。

    Ø 文件元数据,用于描述Schema。

    Ø 16字节的文件同步记号。

    Ø 其中,文件元数据的格式为:

                       i.             值为-1的长整型,标明这是一个元数据块。

                     ii.             标识块长度的长整型。

                   iii.             标识块中key/value对数目的长整型。

                    iv.             每一个key/value对的string key和bytesvalue。

                      v.             标识块中字节总数的4字节长的整数。

    文件数据块:

    数据是以块结构停止组织的,一个文件可以包含一个或者多个文件数据块。

    Ø 表现文件中块中对象数目的长整型。

    Ø 表现块中数据序列化后的字节数长度的长整型。

    Ø 序列化的对象。

    Ø 16字节的文件同步记号。

    当数据块的长度为0时即为文件数据块的最后一个数据,此后的所有数据被自动忽略。

    下图示对象容器文件的结构分解及说明:

 数据和schema

    一个存储文件由两部分组成:头信息(Header)和数据块(Data Block)。而头信息又由三部分构成:四个字节的前缀,文件Meta-data信息和随机生成的16字节同步标记符。Avro目前支撑的Meta-data有两种:schema和codec。

    codec表现对前面的文件数据块(File Data Block)采取何种压缩方式。Avro的实现都需要支撑下面两种压缩方式:null(不压缩)和deflate(使用Deflate算法压缩数据块)。除了文档中认定的两种Meta-data,用户还可以自定义适用于自己的Meta-data。这里用long型来表现有多少个Meta-data数据对,也是让用户在实际应用中可以定义足够的Meta-data信息。对于每对Meta-data信息,都有一个string型的key(需要以“avro.”为前缀)和二进制编码后的value。对于文件中头信息当前的每个数据块,有这样的结构:一个long值记录当前块有多少个对象,一个long值用于记录当前块经过压缩后的字节数,真正的序列化对象和16字节长度的同步标记符。由于对象可以组织成不同的块,使用时就可以不经过反序列化而对某个数据块停止操纵。还可以由数据块数,对象数和同步标记符来定位损坏的块以确保数据完整性。

    三、RPC实现

    当在RPC中使用Avro时,服务器和客户端可以在握手连接时交换模式。服务器和客户端有彼此全部的模式,因此相同定名字段、缺失字段和多余字段等信息之间通信中需要处置的分歧性问题就可以轻易解决。如图2所示,协议中定义了用于传输的消息,消息使用框架后放入缓冲区中停止传输,由于传输的初始就交换了各自的协议定义,因此即使传输双方使用的协议不同所传输的数据也能够准确剖析。

    数据和schema

    图表 2

    Avro作为RPC框架来使用。客户端希望同服务器端交互时,就需要交换双方通信的协议,它相似于模式,需要双方来定义,在Avro中被称为消息(Message)。通信双方都必须保持这种协议,以便于剖析从对方发送过去的数据,这也就是传说中的握手阶段。 

    消息从客户端发送到服务器端需要经过传输层(Transport Layer),它发送消息并接收服务器端的响应。达到传输层的数据就是二进制数据。通常以HTTP作为传输模型,数据以POST方式发送到对方去。在Avro中,它的消息被封装成为一组缓冲区(Buffer),相似于下图的模型:

    数据和schema

    如上图,每个缓冲区以四个字节扫尾,旁边是多个字节的缓冲数据,最后以一个空缓冲区开头。这种机制的利益在于,发送端在发送数据时可以很方便地组装不同数据源的数据,接收方也可以将数据存入不同的存储区。还有,当往缓冲区中写数据时,大对象可以独有一个缓冲区,而不是与其它小对象混合存放,便于接收方方便地读取大对象。

    对象容器文件是Avro的数据存储的详细实现,数据交换则由RPC服务供给,与对象容器文件相似,数据交换也完整依附Schema,所以与Hadoop目前的RPC不同,Avro在数据交换之前需要通过握手进程先交换Schema。

    1、 握手进程

    握手的进程是确保Server和Client取得对方的Schema定义,从而使Server能够准确反序列化请求信息,Client能够准确反序列化响应信息。一般的,Server/Client会缓存最近使用到的一些协议格式,所以,大多数情况下,握手进程不需要交换全部Schema文本。

    所有的RPC请求和响应处置都建立在已完成握手的基础上。对于无状态的连接,所有的请求响应之前都附有一次握手进程;对于有状态的连接,一次握手完成,全部连接的生命期内都有效。

    详细进程:

    Client发起HandshakeRequest,其中含有Client本身SchemaHash值和对应Server端的Schema Hash值(clientHash!=null,clientProtocol=null, serverHash!=null)。如果当地缓存有serverHash值则直接填充,如果没有则通过猜想填充。

    Server用如下之一HandshakeResponse响应Client请求:

    (match=BOTH, serverProtocol=null,serverHash=null):当Client发送准确的serverHash值且Server缓存相应的clientHash。握手进程完成,当前的数据交换都遵照本次握手结果。

    (match=CLIENT, serverProtocol!=null,serverHash!=null):当Server缓存有Client的Schema,但是Client请求中ServerHash值不准确。此时Server发送Server端的Schema数据和相应的Hash值,此次握手完成,当前的数据交换都遵照本次握手结果。

    (match=NONE):当Client发送的ServerHash不准确且Server端没有Client Schema的缓存。这种情况下Client需要重新提交请求信息 (clientHash!=null,clientProtocol!=null, serverHash!=null),Server响应 (match=BOTH, serverProtocol=null,serverHash=null),此次握手进程完成,当前的数据交换都遵照本次握手结果。

    握手进程使用的Schema结构如下示。

    {

    "type":"record",

    "name":"HandshakeRequest","namespace":"org.apache.avro.ipc",

    "fields":[

    {"name":"clientHash", "type": {"type": "fixed","name": "MD5", "size": 16}},

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

    {"name":"serverHash", "type": "MD5"},

    {"name":"meta", "type": ["null", {"type":"map", "values": "bytes"}]}

    ]

    }

    {

    "type":"record",

    "name":"HandshakeResponse", "namespace":"org.apache.avro.ipc",

    "fields":[

    {"name":"match","type": {"type": "enum","name": "HandshakeMatch",

    "symbols":["BOTH", "CLIENT", "NONE"]}},

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

    {"name":"serverHash","type": ["null", {"type":"fixed", "name": "MD5", "size": 16}]},

    {"name":"meta","type": ["null", {"type":"map", "values": "bytes"}]}

    ]

    } 

    2、 消息帧格式

    消息从客户端发送到服务器端需要经过传输层,它发送请求并接收服务器端的响应。达到传输层的数据就是二进制数据。通常以HTTP作为传输模型,数据以POST方式发送到对方去。在 Avro中消息首先分帧后被封装成为一组缓冲区(Buffer)。

    数据帧的格式如下:

    Ø 一系列Buffer:

    1、4字节的Buffer长度

    2、Buffer字节数据

    Ø 长度为0的Buffer结束数据帧 

    3、 Call格式

    一个调用由请求消息、结果响应消息或者错误消息组成。请求和响应包含可扩展的元数据,两种消息都按照之前提出的方法分帧。

    调用的请求格式为:

    Ø  请求元数据,一个类型值的映射。

    Ø  消息名,一个Avro字符串。

    Ø  消息参数。参数根据消息的请求定义序列化。

    调用的响应格式为:

    Ø  响应的元数据,一个类型值的映射。

    Ø  一字节的错误标记位。

    Ø  如果错误标记为false,响应消息,根据响应的模式序列化。

    如果错误标记位true,错误消息,根据消息的错误结合模式序列化。 

    四、实例

    1、 当地序列化/反序列化

    user.avsc

    {"namespace":"example.avro",

 "type": "record",

 "name": "User",

 "fields": [

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

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

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

 ]

    }

    Main.java

    public class Main {

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

       User user1 = new User();

       user1.setName("Alyssa");

       user1.setFavoriteNumber(256);

       // Leave favorite color null 

       // Alternate constructor

       User user2 = new User("Ben", 7,"red"); 

       // Construct via builder

       User user3 = User.newBuilder()

                    .setName("Charlie")

                   .setFavoriteColor("blue")

                   .setFavoriteNumber(null)

                    .build();      

       // Serialize user1 and user2to disk

       File file = new File("users.avro");

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

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

       dataFileWriter.create(user1.getSchema(),new File("users.avro"));

       dataFileWriter.append(user1);

       dataFileWriter.append(user2);

       dataFileWriter.append(user3);

       dataFileWriter.close();      

       // Deserialize Usersfrom disk

       DatumReader<User> userDatumReader = newSpecificDatumReader<User>(User.class);

       DataFileReader<User> dataFileReader = newDataFileReader<User>(file, userDatumReader);

       User user = null;

       while (dataFileReader.hasNext()) {

       // Reuse user object bypassing it to next(). This saves us from

       // allocating and garbagecollecting many objects for files with

       // many items.

       user = dataFileReader.next(user);

       System.out.println(user);

       }                  

    }

    }

    2、 RPC

    mail.avsc

    {"namespace":"example.proto",

 "protocol": "Mail",

 "types": [

    {"name": "Message", "type":"record",

     "fields": [

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

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

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

     ]

    }

 ],

 "messages": {

    "send": {

        "request": [{"name": "message","type": "Message"}],

        "response": "string"

    }

 }

    }

    Main.java

    public class Main {

    public static class MailImpl implements Mail {

        // in this simple example just return details of the message

        public Utf8 send(Message message) {

            System.out.println("Sending message");

            return new Utf8("Sending message to " + message.getTo().toString()

                    + " from " +message.getFrom().toString()

                    + " with body " +message.getBody().toString());

        }

    }

    private static Server server;

    private static void startServer() throws IOException {

        server = new NettyServer(new SpecificResponder(Mail.class,new MailImpl()), newInetSocketAddress(65111));

        // the server implements the Mail protocol (MailImpl)

    }

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

        System.out.println("Starting server");

        // usually this would be anotherapp, but for simplicity

        startServer();

        System.out.println("Server started");

        NettyTransceiver client = new NettyTransceiver(new InetSocketAddress(65111));

        // client code - attach to the server and send a message

        Mail proxy = (Mail) SpecificRequestor.getClient(Mail.class, client);

        System.out.println("Client built, got proxy");

        // fill in the Message record and send it

        Message message = new Message();

        message.setTo(new Utf8("127.0.0.1"));

        message.setFrom(new Utf8("127.0.0.1"));

        message.setBody(new Utf8("this is my message"));

        System.out.println("Calling proxy.send with message: " + message.toString());

        System.out.println("Result: " +proxy.send(message));

        // cleanup

        client.close();

        server.close();

    }

    }

文章结束给大家分享下程序员的一些笑话语录: N多年前,JohnHein博士的一项研究表明:Mac用户平均IQ要比PC用户低15%。超过6000多的参加者接受了测试,结果清晰的显示IQ比较低的人会倾向于使用Mac。Mac用户只答对了基础问题的75%,而PC用户却高达83%。

--------------------------------- 原创文章 By 
数据和schema
---------------------------------

目录
相关文章
|
JSON 网络协议 机器人
ROSBridge简介以及理解使用(下)
ROSBridge简介以及理解使用(下)
1768 0
|
XML 安全 网络协议
netconf简介
netconf简要
|
存储 算法 内存技术
多媒体系统简介
一、多媒体系统简介 多媒体系统是指能够处理和展示多种媒体信息的计算机系统。它可以处理和播放音频、视频、图像等多种形式的媒体数据,并提供交互式的操作和用户界面。多媒体系统广泛应用于娱乐、教育、广告、通信等领域。 多媒体系统通常由以下几个组成部分构成: 1. 媒体输入设备:用于将外部的媒体数据输入到计算机系统中,如麦克风、摄像头、扫描仪等。 2. 媒体处理软件:用于对媒体数据进行处理和编辑的软件,如音频编辑软件、视频编辑软件、图像处理软件等。 3. 媒体存储设备:用于存储媒体数据的设备,如硬盘、光盘、闪存等。 4. 媒体输出设备:用于将处理后的媒体数据输出到外部设备或显示器上,如音箱、显示器、投影
326 0
|
监控 Oracle 关系型数据库
oratop的使用简介
oratop是采用类似top命令的方式实时监控oracle数据库,包括rac和non-rac数据库
118 0
|
XML 监控 Devops
CUNIT简介
CUNIT简介
470 0
CUNIT简介
|
存储 JSON 缓存
数据schemaAvro简介
文章结束给大家来个程序员笑话:[M]     最近在研究Thrift和Avro以及它们的区分,通过各种渠道搜集资料,现整顿出有关Avro的一些资料,方便当前参考。     一、弁言     1、 简介     Avro是Hadoop中的一个子项目,也是Apache中一个独立的项目,Avro是一个基于二进制数据传输高性能的旁边件。
1076 0
|
网络协议 开发工具 Android开发
AndroidDevTools简介
来源:http://www.androiddevtools.cn/ Android SDK在线更新镜像服务器 中国科学院开源协会镜像站地址: IPV4/IPV6: http://mirrors.
2903 0
|
网络协议 机器人 Java
ROSBridge简介以及理解使用(上)
ROSBridge简介以及理解使用(上)
2672 0
ROSBridge简介以及理解使用(上)
|
存储 数据安全/隐私保护
TrueLicense简介
原文 TrueLicense是一个开源的证书管理引擎,官网 使用场景:当项目交付给客户之后用签名来保证客户不能随意使用项目 默认校验了开始结束时间,可扩展增加mac地址校验等。 其中还有ftp的校验没有尝试,本文详细介绍的是本地校验 license授权机制的原理: 生成密钥对,方法有很多。
8738 0
|
Android开发 API 测试技术
[译] WorkManager 简介
这篇文章是 WorkManager 系列中的第一篇。我们将探讨 WorkManager 的基础知识,如何以及何时使用它,以及幕后发生了什么。然后我们将深入研究更复杂的用例。
742 0