【Netflix Hollow系列】Hollow数据模型

549 阅读14分钟

image.png

前言

在前面的文章中,我为大家讲解了Hollow的一些常用术语以及Hollow的体系架构。相信大家对Hollow已经有了一个初步的认识。为了能够更好的使用Hollow,本文将详细阐述Hollow的数据模型,重点将包括Schema在Hollow中的应用,以及定义的数据模型如何映射到Schema中。希望通过本文,能够帮助大家理解Hollow的数据模型,举一反三,能够更好的帮助大家在今后的开发中使用和定义数据模型。

@空歌白石 原创。

数据模型

在讲解Hollow的数据模型前,首先让我们来认识下数据模型是什么,为什么要有数据模型。

数据模型是什么

数据模型Data Model)在维基百科中是这样定义的:

在软件工程中,数据模型是定义数据如何输入和输出的一种模型。其主要作用是为信息系统提供数据的定义和格式。数据模型是数据库系统的核心和基础,现有的数据库系统都是基于某种数据模型而建立起来的。

通过上述定义可以看出,数据模型为信息系统或者说计算机使用的,但是需要人来定义,通过输入可以让计算机识别的数据模型,能够更好的发挥计算机的能力,以便于得到人类需要的输出模型。而输入和输出模型,都需要人类能够很容易的理解,以便于能够构建输入以及认识输出结果。

为什么要定义数据模型

在具体的开发实践中,定义数据模型都是在开发系统时首先需要考虑的事项,那么为什么要首先定义数据模型呢?我觉得可以从这几方面考量这个问题:

首先,定义好数据模型后,我们可以建立抽象思维和现实时间的一个映射,能够比较直观准确的模拟现实世界。比如,我们要对一辆小汽车建立数据模型,我们首先要做的是充分了解和认识一辆小汽车,包括但不限于:汽车的品牌,发动机、变速箱、底盘悬架、座椅、尺寸等等,如果是新能源汽车,需要了解它的续航里程、充电速率、是否支持超充等等因素。只有对这辆小汽车有了充分的认识才能建立起思维和现实世界的联系。

其次,建立数据模型能够让其他人或将来的自己更容易理解数据。仍以小汽车为例,如果没有建立数据模型,我们改如何向他人介绍这辆小汽车呢?看起来是很难的,甚至无法实现。

最后,建立数据模型是为了让计算机能够认识一个事物,这点应该比较好理解,通过建立是Model,计算机就能够支持人想表达的事物是什么样子的。这点的典型应用就是面向对象编程。

数据模型的应用

如果想要应用一个数据模型,需要考虑哪些方面呢?大体上包括以下三个方面:

  • 数据结构:存储在数据库中对象类型的集合,作用是描述数据库组成对象以及对象之间的联系。
  • 数据操作:指对数据库中各种对象实例允许指定的操作的集合,包括操作及其相关的操作规则。
  • 数据完整性约束条件:指在给定的数据模型中,数据及其联系所遵循的一组通用的完整性规则,它能保证数据的正确性和一致性。

Schema

在实际开发中,数据模型与一个名词密不可分,那就是schema。那么schema是什么呢?

Schema是元数据的一个抽象集合,包含一套Schema Component,主要是元素和属性的声明,复杂与简单数据类型的定义。这些Schema Component通常是在处理一批Schema Component时被创建的。

以数据库为例,数据库的Schema包括:表、列、数据类型、视图、存储过程、主键、外键、关系等等。(数据库方面的概念,大家可以稍微留意下,这些概念在后续Hollow的数据模型中也会有类似的概念出现。两者结合起来,应该以更容易的理解Hollow的数据模型。)

Hollow数据模型

在详细介绍Hollow数据模型前,让我们先了解下Data Model在整个Hollow架构中的位置。

Hollow通过ProducerConsumer不断的发送Data,反过来,Consumer不断的消费来自ProducerData。而Data Model就是用来定义DataData Model不仅仅定义Data,还会被ProducerConsumer使用,以基于Data Model生成和消费Data。而Data Model最直观的表述就是Schema

Hollow.png

基本类型

由于Hollow是使用Java实现的,因此Hollow的数据模型首先会支持POJO格式,同时也支持通过Schema的方式定义。

Hollow数据模型的基本字段类型:

  • INT: An integer value up to 32-bits
  • LONG: An integer value up to 64-bits
  • FLOAT: A 32-bit floating-point value
  • DOUBLE: A 64-bit floating-point value
  • BOOLEAN: true or false
  • STRING: An array of characters
  • BYTES: An array of bytes
  • REFERENCE: A reference to another specific type. The referenced type must be defined by the schema.

复杂的数据类型包括List、Set、Map。

当模型无法用以上基本类型表达时,需要使用Reference重新定义一个模型,直到可以用基本类型表达。但是这有点需要特别指出,Reference类型并不支持java中的interfaceabstract classObject等类型,需要是通过HollowSchema方式定义的Object类型。

POJO与Schema两者之间是可以相互转换,如下: POJO → Schema

HollowObjectSchema movieSchema = new HollowObjectSchema("Car", 3); 
movieSchema.addField("engine", HollowObjectSchema.FieldType.FLOAT); 
movieSchema.toString();

Schema → POJO

List<HollowSchema> schemas = HollowSchemaParser.parseCollectionOfSchemas(allSchemas); 
HollowWriteStateEngine initializedWriteEngine = HollowWriteStateCreator.createWithSchemas(schemas);

Object Schema

上文中简单介绍了Hollow的数据模型,通过以上的内容可以定义并不复杂的数据模型,但是如果想要实现更为复杂的功能,就需要本小节中介绍的复杂字段类型,包括Primary KeysInlines Fileds, Referenced FiledsGrouping Associated FieldsTransient Fields等。

Primary Keys

很多情况下,我们需要在数据模型中指定某个字段为主键,就像我们在定义数据库表时,一般需要指定一个主键,并且这个主键通常还会建立对应的主键索引。Hollow如何定义主键呢? 使用POJO的方式,我们可以通过@HollowPrimaryKey的注解来定义。如下:

@HollowPrimaryKey(fields={"id"})
public class Car {
    long id:
    String name;
    String brand;
    String engine;
    String seat;
}

使用@HollowPrimaryKey可以定义类型的主键,Hollow会基于此生成相应的primary key index。这样做能够方便的在内存中进行检索,而无需重复的定义Map来实现索引的功能。具体的使用方式如下:

HollowConsumer consumer = ...;
CarPrimaryKeyIndex idx = new CarPrimaryKeyIndex(consumer);
int carId = ...;
Car car = idx.findMatch(carId);

其中CarPrimaryKeyIndex类,可以使用HollowAPIGenerator类生成,关于这部分内容,会在后续的文章中讲述。这里大家了解即可,如果有兴趣的话,可以查看HollowAPIGenerator源码

在添加数据时,一定要特别注意,Primary Keys 并不支持 NULL 值,HolLow认为主键并不需要NULL值。

Inlines Fileds And Referenced Fileds

Hollow通过@HollowInline注解标识某个字段为Inline Fileds

public class Car {
    long id:
    String name;
    @HollowInline String brand;
    String engine;
    String seat;
}

Inline Fileds 有什么作用呢?如上述例子中,我们使用 @HollowInline 标记了品牌类型,当在很多汽车记录中有相同的品牌名称,重复的记录内容将只保留一份,这样的优化,将显著的节省存储成本。

Namespaced Record Type Names

当需要调整Schema中已经定义的字段名称,可以使用@HollowTypeName注解。如下示例,将座椅类型字段从seat调整为Seat

public class Car {
    long id:
    String name;
    @HollowInline String brand;
    String engine;
    @HollowTypeName(name="Seat")
    String seat;
}

@HollowTypeName不仅可以适用于字段上,同样可以作用于类上。

@HollowTypeName(name="SpecialCar")
public class Car {
    long id:
    String name;
    @HollowInline String brand;
    String engine;
    String seat;
}

使用@HollowTypeName在数据模型中引用的其他类型可以重用标记的字段类型。因此,标记再适当的字段上可以减少REFERENCE字段的堆占用开销。为何使用@HollowTypeName能够减少堆占用,我将在下一篇【Hollow内存布局】中详细介绍。

Grouping Associated Fields

让我们丰富下汽车模型的定义,增加仪表盘和轮胎两个属性。如下:

public class Car {
    long id:
    String name;
    String engine;
    String seat;
    String dashboard;
    String tire;
}

一辆小汽车,的仪表盘和座机都属于内饰的部分,因此,我们可以将seatdashboard统一放在interior,如下:

public class Car {
    long id:
    String name;
    String engine;
    String tire;
    Interior interior;
}
public class Interior {
    String seat;
    String dashboard;
}

汽车厂生产小汽车时,内饰很多车型会是相同的,因此我们可以分别引用这些字段。如果我们这样做了,那么每个Car记录(可能有很多)都必须包含对这些字段的两个单独的引用。相反,通过识别这些字段是相关联的并将它们拉到一起,可以节省空间,因为每个Car记录现在只包含对该数据的一个引用。

Transient Fields

某些情况下,我们希望在数据模型中定义某些字段,但是并不想被消费者感知到,在Hollow中我们可以使用@HollowTransient字段完成。如下:

public class Car {
    long id:
    String name;
    String engine;
    String seat;
    String dashboard;
    String tire;
    @HollowTransient internalNo;
}

汽车生产厂商会在研发新的车型时,用内部代号来替代最终发布时的车型,但是这个内部代号并不想被最终消费者明确的感知。这时候我们可以用@HollowTransient实现这个需求。

Hollow Collection Object Schema

上一章节中,介绍了简单的模型定义需要的一些常用注解。本小节中,将介绍Hollow中集合的定义。

List Schema

首先,我们来看最常用的List模型。仍然使用小汽车的模型定义,一辆小汽车有4个车轮。

public class Car {
    long id:
    String name;
    String engine;
    String seat;
    String dashboard;
    List<String> tires;
}

Set Schema

Set也是很常用的集合类型,首先,只是简单定义轮胎,直接使用Set就可以。

public class Car {
    long id:
    String name;
    String engine;
    String seat;
    String dashboard;
    Set<String> tires;
}

如果Set中是引用类型,我们同样可以使用@HollowHashKey注解,计算Hash以便于在判断Set中是否存在重复数据,如下:

public class Car {
    long id:
    String name;
    String engine;
    @HollowHashKey(fields={"diameter", "width"})
    Set<Tire> tires;
    Interior interior;
}
public class Interior {
    String seat;
    String dashboard;
}
public class Tire {
    String diameter;
    String width;
}

Map Schema

Hollow中对Map的使用如下,但需要在Map使用时,定义Map的hash:

public class Car {
    long id:
    String name;
    String engine;
    @HollowHashKey(fields={"diameter", "width"})
    Set<Tire> tires;
    @HollowHashKey(fields="type")
    Map<Type, Interior> interiors;
}
public class Interior {
    String seat;
    String dashboard;
}
public class Tire {
    String diameter;
    String width;
}

Hash Keys

在Set和Map两种类型的集合中,都需要使用@HollowHashKey注解标记Hash的字段,以便于进行对象的去重。@HollowHashKey的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface HollowHashKey {

    /**
     * Returns the field paths of the hash key.
     * <p>
     * An empty array indicates that the ordinal of an element in a set
     * or a key in a map is used as the hash.
     *
     * @return the field paths of the hash key
     */
    String[] fields();
}

循环引用

Hollow禁止循环引用。如下是一个错误示范。

public class A {
    String a;
    B b;
}
public class B {
    String b;
    A a;
}

HollowSchema

上文中我们定义的小汽车的数据模型,都是基于POJO的。但是Hollow并不仅仅是这样,Hollow同样支持基于Schema的定义。 POJO的定义:

@HollowPrimaryKey(fields={"id"})
public class Car {
    long id:
    String name;
    String engine;
    @HollowHashKey(fields={"diameter", "width"})
    Set<Tire> tires;
    Interior interior;
}

Schema的定义:

Car @PrimaryKey(id) {
    long id;
    string name;
    string engine;
    SetOfTire tires;
    Interior interior;
}
SetOfTire Set<Tire> @HashKey(diameter, width);

多数据模型处理

Hollow具有同时处理多种数据模型的能力,当我们成功部署Hollow应用后,可以在Hollow应用中添加多个数据模型。每种数据模型的生产和消费取决于该数据模型的独立逻辑,相互之间并不影响。

在使用HollowSchema定义数据模型时,同样可以在同一个HollowSchema中定义多个数据模型,如在下面的例子中我们定义了Moive、Actor两种数据模型:

Movie @PrimaryKey(id) {
    long id;
    int releaseYear;
    string title;
    SetOfActor actors;
}

SetOfActor Set<Actor> @HashKey(firstname, surname);

Actor {
    long id;
    String firstname;
    String surname;
}

String {
    string value;
}

Hollow数据模型的兼容

业务不会一成不变,因此数据模型也不会一成不变,数据模型大体包含如下的变化情况:

  • 新增字段
  • 删除现有字段
  • 修改字段

当Producer新增了字段情况下,但是Consumer还未升级Schema,此时,Consumer不会处理新增的字段,直接忽略。 当Producer删除了某些字段,Consumer在未升级情况下,原有字段会赋值为NULL。 最为复杂的是修改字段类型,当前Hollow并不支持对字段类型的修改,当Producer修改了字段属性或名称后,Consumer将无法识别。

为保证数据模型的兼容性,在实践中,如果发现无法向后兼容,最高效安全的方案是重新定义的数据模型,大体步骤:

  1. Producer首先上线生产。
  2. 然后Consumer在接入。
  3. 当线上数据稳定后,将Consumer的旧数据模型下线。
  4. 然后Producer再将旧数据模型下线。至此,新的数据模型上线完成。

举一反三

这个章节中我将为大家简单介绍几个同样使用Schema定义的跨语言框架。

Protocol Buffers

相信大家都听说过或使用过Google的Protocol Buffers,官网:developers.google.cn/protocol-bu…

如果想要使用Protocol Buffers,首先需要定义.proto文件。我们如果想定义一辆小汽车,可能包含如下的属性:

message Car {
  required string name = 1;
  required string brand = 2;
  required string engine = 3;
  required string seat = 4;
}

.proto文件就和Hollow的HollowSchema的定义有着异曲同工之处。如何使用定义好的schema呢?通过Google提供的generator工具,可以生成各种开发语言的类。以Java为例的使用如下:

Car h5 = Car.newBuilder()
    .setName("Hs5")
    .setBrand("HongQi")
    .setEngine("6V")
    .setSeat("First")
    .build();
output = new FileOutputStream(args[0]);
h5.writeTo(output);

造轮子

看到这里,不知道大家会不会有个疑问,强如Google、FaceBook、Netflix这样的巨头都在做看着很类似的事情呢?应该也就是经常被讨论的造轮子问题吧。个人认为需要首先考虑两个问题:

  1. 现有的轮子是否满足实际的业务场景?
  2. 现有的轮子是否有着广泛的应用?
  3. 当遇到问题时,是否能够得到快速的响应?

如果以上两个问题都是yes,那么真的没必要再造轮子。但是如果有任意一个不满足,那么造轮子还是很有必要的。当然我们也不能完全排除,为了体现个人或团队超强的技术能力,再造一个更好更优的轮子的可能。

造轮子实际上也是推动技术进步的一种尝试吧,大家觉得呢?

总结

本文详细介绍了什么是数据模型。具体通过介绍Hollow中关于Object Schema、HollowSchema、CollectionSchema,阐述了Hollow是如何实现数据模型的。Hollow Data Model 是学习Hollow的基础,希望本文能够帮助大家更好的学习Hollow。

良好的数据模型只能初步决定一个组件的优劣,数据模型是否足够的优秀,更为关键的是需要关注数据模型的内存布局是如何定义的。我将在下一篇文章中详细介绍Hollow是如何布局数据模型的。

结束语

想要将Hollow介绍清楚,需要比较大的篇幅,大家可以通过我撰写的 Netflix Hollow系列专栏 查看全部已完成的文章。

今天是端午假期,最后祝大家端午安康。