利用 Datastore 在强一致性和最终一致性之间取得平衡

提供一致的用户体验并利用最终一致性模型扩容到大型数据集

本文档讨论如何实现强一致性以提供积极的用户体验,同时利用 Datastore 的最终一致性模型来处理大量数据和用户。

本文档面向希望在 Datastore 上构建解决方案的软件架构师和软件工程师。为便于那些更熟悉关系型数据库,但不怎么熟悉 Datastore 这类非关系型系统的读者理解,本文档引入了关系型数据库中的相似概念。本文档假定您对于 Datastore 有基本的了解。如果是刚开始使用 Datastore,最简单方法是在 Google App Engine 中使用支持的语言之一。如果您没用过 App Engine,我们建议您首先阅读其中一种语言版本的入门指南存储数据部分。虽然示例代码段使用的是 Python,但无需 Python 专业知识也可学习本文档。

注意:虽然本文中的代码段使用的是适用于 Datastore 的 Python DB 客户端库,但目前已不再推荐使用该客户端库。强烈建议开发者在构建新应用时使用 NDB 客户端库,与上述客户端库相比,后者具有多项优势,例如可通过 Memcache API 自动缓存实体。如果您当前使用的是较早的 DB 客户端库,请参阅 DB 到 NDB 的迁移指南

目录

NoSQL 和最终一致性
Datastore 中的最终一致性
祖先查询和实体组
实体组和祖先查询的限制
祖先查询的替代方案
尽可能缩短实现完全一致性的时间
总结
其他资源

NoSQL 和最终一致性

非关系型数据库也称为 NoSQL 数据库,近年来已成为关系型数据库的替代方案。Datastore 是行业中使用最广泛的非关系型数据库之一。2013年,Datastore 每月处理事务达 4.5 万亿个(Google Cloud Platform 博文)。它简化了开发者存储和访问数据的方式。灵活的架构可以自然地与面向对象和脚本语言相映射。Datastore 还可以提供关系型数据库难以实现的功能,例如面对超大规模处理保持高性能运转以及高可靠性。

更习惯关系型数据库的开发者可能会发现设计使用非关系型数据库的系统很难,因为他们可能不太熟悉非关系型数据库的某些特性和规范。虽然 Datastore 编程模型比较简单,但了解这些特性才是重点。最终一致性就是其中一个特性,本文档主要讲解的就是针对最终一致性的编程。

什么是最终一致性?

最终一致性在理论上保证,假如实体未进行任何新的更新,则对实体执行的所有读取操作将最终返回最后更新的值。在采用最终一致性模型的系统中,大家熟知的一个例子是互联网域名系统 (DNS)。DNS 服务器不一定反映最新的值,而是通过互联网在多个目录中缓存和复制这些值。将修改后的值复制到所有 DNS 客户端和服务器需要一定的时间。但是,DNS 系统是一个非常成功的系统,已经成为互联网的基础之一。它具有高可用性,并且已被证明具有极高的可扩缩性,能够为整个互联网上超过一亿台设备进行域名查询。

图 1 说明了采用最终一致性模型进行复制的概念。该图说明尽管副本始终可供读取,但某些副本可能在特定时刻与原始节点上的最新写入不一致。在该图中,节点 A 是原始节点,节点 B 和 C 是副本。

图 1:采用最终一致性模型进行复制的概念说明

相反,传统的关系型数据库是基于强一致性(也称为即时一致性)的概念设计的。这意味着更新后立即查看数据时,实体的所有观察者所看到的数据是一致的。对于许多使用关系型数据库的开发者来说,该特性已成为基本假设。但为了保持强一致性,开发者必须牺牲应用的可扩缩性和性能。简单来说,在更新或复制过程中必须锁定数据,以确保没有其他进程正在更新同一数据。

图 2 展示了采用强一致性模型的部署拓扑和复制过程的概念视图。在此图中,您可以看到副本的值始终与原始节点保持一致,但在更新完成之前却无法访问这些副本。

图 2:采用强一致性模型进行复制的概念说明

平衡强一致性和最终一致性

非关系型数据库最近大受欢迎,尤其是在需要高可扩缩性和高可用性性能的网络应用中。采用非关系型数据库,开发者能够在每个应用的强一致性和最终一致性之间实现最佳平衡,因而能够结合这两者的优点。例如,诸如“知道好友列表中谁在给定时间在线”或“知道有多少用户点赞了您的帖子”之类的用例无需具有强一致性。通过利用最终一致性,可以为这些用例提供可扩缩性和性能。需要强一致性的用例包括“用户是否完成结算进程”或“游戏玩家在战斗期间获得的点数”等信息。

上述示例概括来说就是,具有大量实体的用例常常表明最终一致性是最佳模型。如果查询中有大量结果,则包含或排除特定实体可能不影响用户体验。另一方面,具有少量实体和范围较窄的用例表明需要强一致性。此时用户体验将受到影响,因为具体情境将让用户意识到应该包含或排除哪些实体。

出于上述原因,开发者必须了解 Datastore 的非关系特性。以下部分讨论如何将最终一致性和强一致性模型进行结合,从而构建可扩缩、可用性高的高性能应用。在此过程中,还要满足一致性要求,以获得积极的用户体验。

Datastore 中的最终一致性

当需要强一致性的数据视图时,必须选择正确的 API。表 1 中显示了各种不同的 Datastore 查询 API 及其相应的一致性模型。

Datastore API

读取实体值

读取索引

全局查询

最终一致性

最终一致性

仅限于键的全局查询

不适用

最终一致性

祖先查询

强一致性

高度一致性

按键查找 (get())

强一致性

表 1:Datastore 查询/get 调用以及可能的一致性行为

没有祖先实体的 Datastore 查询称为全局查询,采用最终一致性模型。这并不能保证强一致性。仅限于键的全局查询是只返回匹配查询的实体键而不返回实体属性值的全局查询祖先查询根据祖先实体划分查询范围。以下部分更详细地介绍每种一致性行为。

读取实体值时的最终一致性

除祖先查询外,更新的实体值在执行查询时可能不会立即可见。为了理解读取实体值时最终一致性的影响,请设想一个实体 Player 具有属性 Score 的场景。例如,假设初始 Score 的值为 100。一段时间后,Score 值更新为 200。如果执行全局查询并在结果中包含同一 Player 实体,则返回的实体的属性 Score 的值可能仍然显示为 100。

此行为是由 Datastore 服务器之间的复制造成的。复制由 Datastore 的底层技术 Cloud Bigtable 和 Megastore 管理(请参阅其他资源,了解 Bigtable 和 Megastore 的详情)。Paxos 算法用于执行复制,该算法会同步等待,直到大多数副本已确认更新请求。一段时间之后,使用请求中的数据更新副本。这段时间通常很短,但实际时长不确定。如果在更新完成之前执行查询,则查询可能读取过时的数据。

在很多情况下,更新将很快到达所有副本。但是,当几项因素同时出现时,可能会增加实现一致性的时间。这些因素包括整个数据中心内涉及到数据中心之间大量服务器切换的任何突发状况。鉴于这些因素在不断变化,因此不可能就实现完全一致性提供任何确切的时间要求。

查询返回最新值所需的时间通常很短。但在极少数情况下,当复制延迟时间增加时,该时间可能会长得多。应谨慎设计使用 Datastore 全局查询的应用,以使其能适当地处理这些情况。

可通过使用仅限于键的查询、祖先查询或按键查找(get() 方法)避免读取实体值时的最终一致性。我们将在下面更深入地讨论这些不同类型的查询。

读取索引时的最终一致性

执行全局查询时,索引可能尚未更新。这意味着尽管您可能能够读取实体的最新属性值,但可能会按照旧的索引值过滤查询结果中包含的“实体列表”。

为了理解最终一致性对读取索引的影响,可设想一个将新实体 Player 插入到 Datastore 的场景。该实体有一个属性 Score,其初始值为 300。插入后立即执行仅限于键的查询以获取 Score 值大于 0 的所有实体。您期望看到最近插入的 Player 实体出现在查询结果中。但您可能会意外地发现结果中并未出现 Player 实体。如果未在执行查询时使用新插入的值更新 Score 属性的索引表,则可能出现这种情况。

请记住,Datastore 中的所有查询都是针对索引表执行的,但对索引表的更新是异步的。每个实体更新基本上都由两个阶段组成。第一个阶段是提交阶段,即写入事务日志。第二个阶段是写入数据并更新索引。如果提交阶段成功,则写入阶段一定成功,但可能不立即发生。如果在更新索引之前查询实体,则最终看到的可能是尚未一致的数据。

该两阶段过程导致在一定时间后才能在全局查询中看到实体的最新更新。与实体值最终一致性相似,该时间延迟通常很短,但也可能较长(在异常情况下甚至达到数分钟或更长时间)。

更新后也可能发生这种情况。例如,假设您使用新的 Score 属性值 0 来更新现有实体 Player,并在之后立即执行相同的查询。您期望查询结果中不显示该实体,因为新的 Score 值 0 会将其排除。但由于前面所述的异步索引更新行为,该实体仍可能包含在结果中。

只能使用祖先查询或按键查找方法来避免读取索引时的最终一致性。仅限于键的查询无法避免这种行为。

读取实体值和索引时的强一致性

在 Datastore 中,只有两个 API 为读取实体值和索引提供了强一致性的视图:(1) 按键查找方法和 (2) 祖先查询。如果应用逻辑需要强一致性,开发者应该在这两种方法中任选其一从 Datastore 中读取实体。

Datastore 专用于在这些 API 上提供强一致性。在调用其中任一 API 时,Datastore 将清空其中一个副本和索引表上所有待处理的更新,然后执行查找或祖先查询。因此,系统将始终根据更新后的索引表返回最新的实体值,使其具有基于最新更新的值。

与查询相比,按键查找调用只返回由一个键或一组键指定的一个实体或一组实体。这意味着祖先查询是 Datastore 中唯一满足强一致性要求和过滤要求的方法。但仅在指定实体组的情况下,才能使用祖先查询。

祖先查询和实体组

正如本文开头所述,Datastore 的一项优势是让开发者可在强一致性和最终一致性之间找到最佳平衡。在 Datastore 中,实体组是一个具有强一致性、事务性和局部性的单元。利用实体组,开发者可定义应用中实体之间的强一致性范围。通过这种方式,应用可在实体组内保持一致性,同时作为一个完整的系统实现高可扩缩性、高可用性和高性能。

实体组是由根实体及其子实体或后代实体形成的一个层次结构。[1]为创建实体组,开发者指定了一个祖先路径,该路径实际上是一系列作为子键前缀的父键。实体组的概念如图 3 所示。在该示例中,键为“ateam”的根实体具有两个子实体,它们的键分别为“ateam/098745”和“ateam/098746”。

图 3:实体组概念的示意图

在实体组内,保证了以下特性:

  • 强一致性
    • 对实体组进行的祖先查询将返回强一致的结果。通过这种方式,查询可反映按最新索引状态过滤的最新实体值。
  • 事务性
    • 通过以编程方式界定事务,实体组在事务中提供 ACID(原子性、一致性、隔离性和持久性)特性。
  • 地理位置
    • 实体组中的实体将存储在 Datastore 服务器上物理邻近的位置,因为所有实体均按键的字典顺序进行排序和存储。这使得祖先查询能够以最少的 I/O 快速扫描实体组。

祖先查询是一种只针对指定的实体组执行的特殊查询形式。它采用强一致性模型执行查询。在后台,Datastore 确保在执行查询之前应用所有待处理的复制和索引更新。

祖先查询示例

本部分介绍如何在实践中使用实体组和祖先查询。在下例中,我们考虑了为人们管理数据记录的问题。假设代码添加了一个特定种类的实体,随后我们立即对该种类进行查询。下面的示例 Python 代码演示了这个概念。

# Define the Person entity
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
    organization = db.StringProperty()
# Add a person and retrieve the list of all people
class MainPage(webapp2.RequestHandler):
    def post(self):
        person = Person(given_name='GI', surname='Joe', organization='ATeam')
        person.put()
        q = db.GqlQuery("SELECT * FROM Person")
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname,
                        'organization': p.organization})

此代码的问题是,在大多数情况下,查询不会返回在它上面的语句中添加的实体。由于插入后立即进行查询,因此查询时不更新索引。但此用例的有效性也存在问题:是否真的需要在没有具体情境的情况下用一个页面返回所有人员列表?如果有一百万人呢?该页面需要很长时间才能返回。

该用例的性质表明我们应该提供一些具体情境来缩小查询范围。在本例中,我们要使用的情境是组织。如果这样做,我们可将组织用作实体组并执行祖先查询,这就解决了一致性问题。下面的 Python 代码演示了这一点。

class Organization(db.Model):
    name = db.StringProperty()
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
class MainPage(webapp2.RequestHandler):
    def post(self):
        org = Organization.get_or_insert('ateam', name='ATeam')
        person = Person(parent=org)
        person.given_name='GI'
        person.surname='Joe'
        person.put()
        q = db.GqlQuery("SELECT * FROM Person WHERE ANCESTOR IS :1 ", org)
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname})

这一次,在 GqlQuery 中指定了祖先组织,因此查询返回刚才插入的实体。该示例可进行扩展,通过查询人名并在查询中包含祖先来列出某个具体人员。或者,也可以通过保存实体键,然后使用它通过按键查找来列出具体个人。

保持 Memcache 和 Datastore 之间的一致性

实体组还可以用作保持 Memcache 条目和 Datastore 实体之间一致性的单元。例如,设想一个您统计每个团队中的人员数量并将其存储在 Memcache 中的场景。为确保缓存的数据与 Datastore 中的最新值一致,可以使用实体组元数据。元数据返回所指定的实体组的最新版本号。您可将该版本号与 Memcache 中存储的编号进行比较。使用此方法,您可通过读取一组元数据来检测整个实体组中任何实体的变化,而不用扫描组中的所有单个实体。

实体组和祖先查询的限制

使用实体组和祖先查询的方法并不是万能的。在实践中存在两个难点,使得这种技术难以获得普遍应用,如下所列。

  1. 每个实体组的每秒写入仅更新一次。
  2. 创建实体后,实体组关系无法更改。

写入限制

一大难点是系统必须设计为能容纳每个实体组中更新(或事务)的数量。支持的限制是每个实体组每秒更新一次。[2]如果更新数量需要超过该限制,则实体组可能成为性能瓶颈。

在上例中,每个组织可能需要更新组织中所有人的记录。请设想以下场景:“ateam”中有 1000 人,每人每秒可能对任何属性更新一次。因此,实体组中每秒最多可能有 1000 次更新,而由于更新限制,可能无法实现此结果。这说明有必要选择一个考虑到性能要求的适当的实体组设计。这是在最终一致性和强一致性之间找到最佳平衡的难点之一。

实体组关系的不变性

另一个难点是实体组关系的不变性。实体组关系是基于键命名而静态形成的。创建实体后无法更改。要更改关系,只能删除实体组中的实体,再重新创建这些实体。这样,我们就无法使用实体组动态定义一致性或事务性的临时范围。相反,一致性和事务性的范围与设计时定义的静态实体组紧密相关。

例如,请设想一个您希望在两个银行账户之间进行电汇的场景。该业务场景需要强一致性和事务性。但这两个账户终究不能归入一个实体组,也不能基于全局父级。该实体组将遏制整个系统的性能,阻碍执行其他电汇请求。因此不能以这种方式使用实体组。

还有一种方法能够以具备高可扩缩性和高可用性的方式实现电汇。您可为每个账户创建一个实体组,而不是将所有账户放到一个实体组中。这样,您可使用事务确保对两个银行账户都进行了 ACID 更新。事务是 Datastore 的一项功能,让您能够为多达 25 个实体组创建具有 ACID 特性的操作集。请注意,在事务中必须使用强一致的查询,例如按键查找和祖先查询。如需详细了解事务限制,请参阅事务和实体组

祖先查询的替代方案

如果您的现有应用具有大量实体存储在 Datastore 中,则以后可能很难在重构中采用实体组。重构时,需要删除所有实体并将其添加到实体组关系中。因此,在 Datastore 的数据建模中,请务必在应用设计的早期阶段决定实体组的设计。否则,您可能在重构中受到限制,无法使用其他替代方案来实现某种级别的一致性,例如无法在仅限于键的查询之后使用按键查找或无法使用 Memcache。

仅限于键的全局查询结合按键查找

仅限于键的全局查询是一种特殊类型的全局查询,只返回没有实体属性值的键。由于返回值只有键,因此查询时实体值不会遇到可能的一致性问题。仅限于键的全局查询结合按键查找方法,将读取最新的实体值。但需注意的是,仅限于键的全局查询无法消除在查询时索引不一致的可能性,这可能导致根本无法检索到实体。系统可能通过过滤掉旧的索引值而生成查询结果。总之,只有在应用要求允许索引值在查询时不一致时,开发者才可以使用仅限于键的全局查询,然后使用按键查找。

使用 Memcache

虽然 Memcache 服务不稳定,但具有强一致性。因此,通过结合 Memcache 查找和 Datastore 查询,可构建一个在大多数情况下尽可能减少一致性问题的系统。

例如,请设想一个游戏应用的场景,该应用维护一个 Player 实体列表,每个实体的得分大于零。

  • 对于插入或更新请求,将其应用到 Memcache 中的 Player 实体列表以及 Datastore。
  • 对于查询请求,从 Memcache 中读取 Player 实体列表;当 Memcache 中没有该列表时,在 Datastore 上执行仅限于键的查询。

只要 Memcache 中存在缓存列表,返回的列表就具有一致性。如果条目已被逐出,或者 Memcache 服务暂时不可用,则系统可能需要从可能返回不一致结果的 Datastore 查询中读取值。此项技术可应用于各种容许少量不一致的应用。

将 Memcache 用作 Datastore 的缓存层时,可采用以下最佳做法:

  • 捕获 Memcache 异常和错误,进而保持 Memcache 值和 Datastore 值之间的一致性。如果在更新 Memcache 上的条目时收到异常,请确保使 Memcache 中的旧条目无效。否则,某个实体可能存在不同的值(Memcache 中的旧值和 Datastore 中的新值)。
  • 在 Memcache 条目上设置到期时间。建议将每个条目的到期时间设置得较短,以尽量降低 Memcache 异常情况下出现不一致的可能性。
  • 更新条目时使用比较和设置功能实现并发控制。这有助于确保同时更新同一条目时不互相干扰。

逐步迁移到实体组

上一部分中提出的建议只是降低了不一致行为的可能性。当需要强一致性时,建议基于实体组和祖先查询来设计应用。但迁移现有应用可能包括将现有数据模型和应用逻辑从全局查询更改为祖先查询,因此并不可行。要实现此目标,一种方法是采用过渡转换过程,如下所示:

  1. 确定应用中需要强一致性的功能并确定其优先级。
  2. 使用实体组写入 insert() 或 update() 函数的新逻辑,以补充(而不是替换)现有逻辑。这样,就可使用适当的函数处理新实体组和旧实体上的任何新的插入或更新操作。
  3. 修改读取或查询函数的现有逻辑:如果请求存在新的实体组,则先执行祖先查询;如果不存在新的实体组,则执行旧的全局查询作为回退逻辑。

该策略允许从现有数据模型逐步迁移到基于实体组的新数据模型,从而最大限度地降低由最终一致性引起的问题风险。在实践中,该方法取决于它应用于实际系统的具体用例和要求。

回退到降级模式

目前,当应用的一致性劣化时,很难以编程方式检测到这种状况。但是,如果您恰好可通过其他方式确定应用出现一致性劣化的情况,则可以实现一个降级模式;该模式可打开或关闭,目的是禁用某些需要强一致性的应用逻辑区域。例如,可以在特定屏幕中显示维护消息,而不是在结算报告屏幕上显示不一致的查询结果。这样,应用中的其他服务可照常工作,从而减少对用户体验的影响。

尽可能缩短实现完全一致性的时间

在拥有数百万用户或 TB 级 Datastore 实体的大型应用中,如果对 Datastore 的使用不当,可能会导致一致性劣化。不当的使用方式包括:

  • 实体键中采用顺序编号
  • 索引太多

这些做法不会对小型应用造成影响。但是,一旦应用规模变得非常大,它们就会导致系统有更大可能性需要更长时间才能实现一致性。因此建议在应用设计的早期阶段就避免这些做法。

反模式 1:实体键顺序编号

在 App Engine SDK 1.8.1 发布之前,Datastore 使用具有一般连续模式的小整数 ID 序列作为默认的自动生成键名。在部分文档中,这被称为“旧政策”,用于创建具有未指定应用的键名的任何实体。这种旧政策生成的实体键名具有顺序编号,例如 1000、1001、1002。但是,如前述讨论,Datastore 按照键名的字典顺序存储实体,让这些实体非常可能存储在相同的 Datastore 服务器上。如果应用吸引巨大的流量,则这种顺序编号可能导致集中操作特定服务器,从而可能需要更长时间来实现一致性。

在 App Engine SDK 1.8.1 中,Datastore 引入了一种新的 ID 编号方法,其默认政策使用分散型 ID(请参阅参考文档)。这种默认政策会生成一个随机序列,其中 ID 的长度可达 16 位且每个数字大致均匀分布。使用这种政策时,大型应用的流量可能更好地分布在一组 Datastore 服务器中,从而缩短实现一致性所需的时间。建议使用默认政策,除非应用明确要求与旧政策兼容。

如果在实体上显式设置键名,则应将命名方案设计为在整个键名空间上均匀地访问实体。换句话说,不要集中访问特定范围,因为它们是按键名的字典顺序排序的。否则,可能出现与顺序编号相同的问题。

要理解按不均匀分布的方式访问键空间,请设想一个使用顺序键名创建实体的示例,如以下代码所示:

p1 = Person(key_name='0001')
p2 = Person(key_name='0002')
p3 = Person(key_name='0003')
...

应用访问模式可能通过特定范围的键名创建“热点”,例如集中访问最近创建的 Person 实体。此情况下,经常访问的键都将具有更高级别的 ID。随后,负载可能集中在特定的 Datastore 服务器上。

反过来,要理解按均匀分布的方式访问键空间,请设想使用较长的随机字符串作为键名。相关说明见下例:

p1 = Person(key_name='t9P776g5kAecChuKW4JKCnh44uRvBDhU')
p2 = Person(key_name='hCdVjL2jCzLqRnPdNNcPCAN8Rinug9kq')
p3 = Person(key_name='PaV9fsXCdra7zCMkt7UX3THvFmu6xsUd')
...

现在,最近创建的 Person 实体将分散在键空间和多个服务器上。假设有足够多的 Person 实体。

反模式 2:索引太多

在 Datastore 中,更新一次实体将导致为该实体种类定义的所有索引均进行更新。如果应用使用很多自定义索引,则一次更新可能涉及对索引表进行数十、数百甚至数千次更新。在大型应用中,过度使用自定义索引可能增加服务器的负载,还可能使实现一致性需要更长的时间。

在多数情况下,添加自定义索引以支持客户服务、问题排查或数据分析任务等要求。BigQuery 是一个扩缩能力极强的查询引擎,能够在没有预建索引的情况下对大型数据集执行临时查询。该引擎比 Datastore 更适合需要复杂查询的使用场景,例如客户服务、问题排查或数据分析。

一种做法是结合使用 Datastore 和 BigQuery 以满足不同的业务需求。使用 Datastore 进行核心应用逻辑所需的在线事务处理 (OLTP),而使用 BigQuery 进行后端操作所需的在线分析处理 (OLAP)。要移动这些查询所需的数据,可能需要将 Datastore 中的数据连续导出到 BigQuery。

除了替代实施自定义索引外,推荐的另一种方法是明确指定未编入索引的属性(请参阅属性和值类型)。默认情况下,Datastore 将为实体种类的每个可索引属性创建一个不同的索引表。如果某个种类有 100 个属性,则该种类将有 100 个索引表,且实体每次更新时将额外更新 100 次。如果查询条件不需要某些属性,则最佳做法是尽可能地将这些属性设置为未编入索引。

除了实现一致性时延迟的可能性降低以外,这些索引优化还能大幅降低大量使用索引的大型应用中的 Datastore 存储成本

总结

最终一致性是非关系型数据库的基本要素,开发者可通过此类数据库在可扩缩性、性能和一致性之间找到最佳平衡。请务必了解如何处理最终一致性和强一致性之间的平衡,进而设计出适合您的应用的最佳数据模型。在 Datastore 中,要在实体范围内保证强一致性,最佳方式是使用实体组和祖先查询。如果应用由于文中所述限制无法采用实体组,可考虑使用其他选项,例如使用仅限于键的查询或 Memcache。对于大型应用,请利用最佳做法(例如使用分散的 ID 和减少索引)缩短实现一致性所需的时间。可能还有必要结合使用 Datastore 和 BigQuery,进而满足对复杂查询的业务需求并尽可能减少对 Datastore 索引的使用。

其他资源

如需详细了解本文档中讨论的主题,可参阅以下资源:




[1] 实体组甚至可通过仅指定根实体或父实体的一个键来形成,而不存储实际根实体或父实体,因为实体组函数都是基于键之间的关系实现的。

[2] 支持的限制是在事务之外每个实体组每秒更新一次,或每个实体组每秒一次事务。如果将多个更新汇总到一个事务中,那么您将受到 10 MB 的最大事务大小以及 Datastore 服务器的最大写入速率的限制。