本文档介绍了存储在 Datastore 中的对象的数据模型、如何使用 API 构建查询以及如何处理事务。
实体
Datastore 中的对象称为实体。实体具有一个或多个命名的属性,且每个属性可具有一个或多个值。属性值可以属于多种数据类型,包括整数、浮点数、字符串、日期和二进制数据等等。在对具有多个值的属性进行查询时,系统会测试是否有任何值满足查询条件。这使得此类属性对成员资格测试很有用。
种类、键和标识符
每个 Datastore 实体均属于特定“种类”,如此可将实体进行分类以便于查询;例如,人力资源应用可以通过种类为 Employee
的实体来表示公司的每位员工。此外,每个实体都有唯一标识它的“键”。键由以下部分组成:
- 实体的种类
- “标识符”,可以是以下任何一种
- 键名字符串
- 整数 ID
- (可选)祖先路径,用于在 Datastore 层次结构中确定实体的位置
标识符是在创建实体时分配的。标识符是实体键的一部分,因此与实体永久关联且不可更改。标识符可通过下述两种方式分配:
- 应用可为实体指定自己的键名字符串。
- 您可让 Datastore 自动为实体分配整数数字 ID。
祖先路径
Cloud Datastore 中的实体形成一个与文件系统目录结构类似的层级结构空间。创建实体时,您可选择指定另一实体作为其父实体;新实体是父实体的子实体(请注意,与文件系统不同,无需实际存在父实体)。没有父实体的实体是根实体。实体与其父实体之间的关联是永久的,实体创建后就无法更改。Cloud Datastore 绝不向父实体相同的两个实体分配同一数字 ID,也不分配给两个根实体(即没有父实体的实体)。
实体的父实体、父实体的父实体和以此类推得出的实体都是该实体的祖先实体;而实体的子实体和子实体的子实体等都是它的后代实体。根实体及其所有后代实体都属于同一个实体组。实体序列从根实体开始,接着从父实体到子实体,再指向给定的实体,这就构成了实体的祖先路径。识别实体的完整键由一系列种类/标识符对构成,它们指定实体的祖先路径并以实体自身的种类/标识符对终止。
[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]
对于根实体,祖先路径为空,且键仅由实体自身的种类和标识符组成:
[Person:GreatGrandpa]
此概念如下图所示:
查询和索引
除了直接按实体的键从 Datastore 中检索实体之外,应用还可以执行查询来按实体的属性值进行检索。 查询可对给定种类的实体执行操作;它可以针对实体的属性值、键和祖先实体指定过滤条件,并可返回零个或零个以上实体作为结果。查询还可指定排序顺序,以便按实体的属性值对结果进行排序。结果包括符合以下条件的所有实体:针对过滤条件和排序顺序中指定的每个属性均至少有一个值(可能为 null),并且其属性值符合所有指定的过滤条件。查询可以返回整个实体、投影的实体,也可以仅返回实体键。
典型的查询包括以下内容:
执行时,查询会检索给定种类的实体中满足所有给定过滤条件的所有实体,并按指定顺序排序。查询以只读方式执行。注意:为了节约内存并提高性能,查询应尽可能指定对返回结果数量的限制。
查询还可以包括一个祖先过滤条件,用于将结果限制为来自指定祖先实体的实体组。这种查询称为祖先查询。默认情况下,祖先查询会返回强一致性的结果,这些结果肯定是最新的,包含数据的最新更改。相比之下,非祖先查询可以在整个 Datastore 范围内(而不仅仅是单个实体组)执行,但只能保证最终一致性,并且可能返回过时的结果。如果强一致性对您的应用很重要,您可能需要在设计数据结构时考虑到这一点,将相关实体放在同一个实体组中,以便可以使用祖先查询而不是非祖先查询来检索它们。如需了解详情,请参阅设计数据结构以确保强一致性。
App Engine 会为实体的每个属性预定义简单索引。App Engine 应用可以在名为 datastore-indexes.xml
的索引配置文件中定义更多的自定义索引,该文件在应用的 /war/WEB-INF/appengine-generated
目录中生成。开发服务器遇到无法使用现有索引执行的查询时,会自动向此文件添加建议。
在上传应用之前,可以通过编辑该文件来手动优化索引。
注意:基于索引的查询机制支持多种查询,适用于大多数应用,但它不支持其他数据库技术中常见的某些种类的查询:特别是 Datastore 查询引擎不支持联接和聚合查询。如需了解 Datastore 查询的相关限制,请参阅 Datastore 查询页面。
交易
每次尝试插入、更新或删除实体时,这些操作都将在事务环境中进行。单个事务可以包括任意数量的此类操作。为保持数据的一致性,事务确保其包含的所有操作作为一个单元应用于 Datastore,如果任何操作失败,则不应用任何操作。
您可以在单个事务中对实体执行多个操作。例如,要增加某对象中的计数器字段,您需要读取计数器的值,计算新值,然后将其存储回来。如果没有事务,则另一个进程可能会在您读取值与更新值期间递增计数器,从而导致应用覆盖更新的值。在单个事务中执行读取、计算和写入可确保没有其他进程干扰增量。
事务和实体组
事务中只允许祖先实体查询,即每个事务性查询都必须限制为单个实体组。事务本身可应用于多个实体,这些实体可以属于单个实体组,在跨组事务的情况下也可属于不同的实体组(不超过 25 个)。
Datastore 使用乐观并发控制机制管理事务。当两个或多个事务尝试同时更改同一实体组(更新现有实体或创建新实体)时,第一个提交的事务将成功,所有其他事务将无法提交。然后可以对更新的数据重试其他这些事务。请注意,这会限制您可以对给定实体组中的任何实体执行的并发写入数量。
跨组事务
针对属于不同实体组的实体所执行的事务称作“跨组 (XG) 事务”。该事务最多可以应用于 25 个实体组,只要没有并发事务触及它所应用的任何实体组,该事务就会成功。这为您提供了更大的灵活性来组织数据,因为对于种类完全不同的数据,您无需仅仅是为了对其执行原子写入,而强行将这些数据放在同一个祖先实体下。
与在单组事务中一样,您无法在跨组事务中执行非祖先实体查询。不过,您可以在单独的实体组上执行祖先实体查询。非事务性(非祖先实体)查询可能会看到先前提交的事务的全部或部分结果,或者看不到任何结果。(如需了解此问题的背景信息,请参阅 Datastore 写入和数据可见性。)不过,这种非事务性查询更有可能返回部分提交的跨组事务的结果,而不是部分提交的单组事务的结果。
仅触及单个实体组的跨组事务与单组非跨组事务具有相同的性能和费用。对于触及多个实体组的跨组事务,其产生的操作费用与执行同样操作的非跨组事务是相同的,但延迟时间可能比后者更长。
Datastore 写入和数据可见性
数据分两个阶段写入 Datastore:
- 在提交阶段,实体数据记录在大多数副本的事务日志中,而未记录实体数据的任何副本都会被标记为没有最新日志。
- 应用阶段在每个副本中独立发生,由两个并行执行的操作组成:
- 实体数据写入该副本。
- 实体的索引行写入该副本。(请注意,这可能比写入数据本身所花的时间更长。)
写入操作在提交阶段之后立即返回,然后应用阶段会异步发生,应用阶段可能会于不同的时间在每个副本中发生,并且可能会在提交阶段完成后延迟几百毫秒或更长时间发生。如果提交阶段发生故障,则系统会自动重试;但如果故障继续存在,则 Datastore 会返回一条提示异常的错误消息,而您的应用会收到这一消息。如果提交阶段成功但应用阶段在特定副本中失败,那么在发生以下任一情况时,应用阶段将在该副本中前滚直至完成:
- Datastore 的定期清除功能检查未完成的提交作业并应用它们。
- 针对受影响的实体组执行的某些操作(
get
、put
、delete
和祖先查询)导致了更改,如果这些更改已提交但尚未应用,那么系统在继续新操作之前,必须先在执行操作的副本中完成这些更改。
这种写入行为可能会在提交阶段和应用阶段中的不同环节,对应用如何及何时看到数据产生诸多影响:
- 如果写入操作报告超时错误,则系统无法确定该操作是成功还是失败(如果没有尝试读取数据的话)。
- 由于 Datastore 的 get 查询和祖先查询会向正在执行查询的副本应用未完成的修改,因此这些操作始终会看到包含先前所有成功事务的一致视图。这意味着
get
操作(通过更新后实体的键查找该实体)一定会看到该实体的最新版本。 - 非祖先查询可能会返回过时的结果,原因是这些查询可能是在尚未应用最新事务的副本上执行的。即使执行的操作保证会应用未完成的事务,也可能会发生这种情况,因为查询可能会在不同于先前操作的副本上执行。
- 并发更改的时间可能会影响非祖先查询的结果。如果某个实体最初满足查询,但后来发生更改,不再满足查询,那么当这些更改尚未应用于执行查询的副本中的索引时,查询的结果集中仍可能会包含该实体。
Datastore 统计信息
Datastore 保有应用存储数据的相关统计信息,例如给定种类的实体数量,或者给定类型的属性值使用的空间大小。您可以在 Google Cloud 控制台的 Datastore 信息中心页面中查看这些统计信息。您还可以通过查询特定命名的实体,在应用内使用 Datastore API 以编程方式访问这些值;如需了解详情,请参阅 Java 8 中的 Datastore 统计信息。