构建数据以实现高度一致性

Datastore 可将数据分布到多台机器,并在广泛的地理区域中使用同步复制,因而具备高可用性、扩缩性和耐用性。但是,此设计也有所折衷,即任何单个实体组的写入吞吐量均被限制为大约每秒提交一次,并且跨多个实体组进行查询或执行事务也受到限制。本页面详细介绍了这些限制,并讨论了设计数据结构的最佳做法,以便在支持高度一致性的同时仍满足应用的写入吞吐量要求。

具备高度一致性的读取始终返回最新数据,如果在事务中执行此类操作,则返回的数据看上去来自单个一致的快照。不过,为了确保高度一致性或参与事务,查询必须指定祖先实体过滤条件,并且事务最多可涉及 25 个实体组。具备最终一致性的读取没有这些限制,并且足以应对大多数情况。借助具备最终一致性的读取,您可以将数据分布到更多的实体组中,并对不同实体组并行执行提交操作,从而实现更高的写入吞吐量。不过,您需要了解具备最终一致性的读取的特性,以确定其是否适合您的应用:

  • 这些读取操作的结果可能不会反映最新的事务。发生这种情况的原因是,这些读取操作并不能确保它们是针对最新副本运行。相反,它们会使用在执行查询时该副本上可用的任何数据。复制延迟时间通常不会超过几秒钟。
  • 跨多个实体的已提交事务可能看上去已应用于这些实体中的一部分,但未应用于其他实体。但请注意,事务永远不会在单个实体中显示为部分应用。
  • 查询结果可能包含不符合过滤条件的实体,还可能排除符合过滤条件的实体。发生这种情况是因为,从中读取索引的版本可能与从中读取实体本身的版本不同。

要了解如何设计数据结构以确保高度一致性,请对用于一个简单留言板应用的两种不同方法进行比较。第一种方法是为创建的每个实体创建一个新的根实体:

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // No parent key specified, so Greeting is a root entity.
  Entity greeting = new Entity("Greeting");
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}

然后,该方法会对实体种类 Greeting 查询最新的十条问候。

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Query query = new Query("Greeting").addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

不过,由于您使用的是非祖先查询,因此在该方案中,用于执行查询的副本在执行查询的时候可能尚未发现新的问候。尽管如此,几乎所有写入都可在提交几秒钟之后用于非祖先查询。对许多应用而言,在当前用户自己更改的情况下,提供非祖先查询结果的解决方案通常足以将此类复制延迟时间控制在完全可接受的范围。

如果强一致性对于您的应用非常重要,另一种方法是写入具有祖先实体路径的实体,该路径标识必须在具备强一致性的单个祖先查询中读取的所有实体的同一根实体:

protected Entity createGreeting(
    DatastoreService datastore, User user, Date date, String content) {
  // String guestbookName = "my guestbook"; -- Set elsewhere (injected to the constructor).
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);

  // Place greeting in the same entity group as guestbook.
  Entity greeting = new Entity("Greeting", guestbookKey);
  greeting.setProperty("user", user);
  greeting.setProperty("date", date);
  greeting.setProperty("content", content);

  datastore.put(greeting);
  return greeting;
}

然后,您将能够在由该公共根实体标识的实体组内执行具备高度一致性的祖先查询:

protected List<Entity> listGreetingEntities(DatastoreService datastore) {
  Key guestbookKey = KeyFactory.createKey("Guestbook", guestbookName);
  Query query =
      new Query("Greeting", guestbookKey)
          .setAncestor(guestbookKey)
          .addSort("date", Query.SortDirection.DESCENDING);
  return datastore.prepare(query).asList(FetchOptions.Builder.withLimit(10));
}

此方法会以每个留言板一个实体组的方式写入实体组,因此能够实现高度一致性,但也会限制对留言板的更改,即每秒钟不超过 1 次写入(支持的实体组限制)。如果应用有可能遇到更频繁的写入使用情况,您可能需要考虑使用其他方式:例如,您可以将最近发布的帖子放入设有过期时间的 Memcache 中,并混合显示来自 Memcache 和 Datastore 的最新帖子,或者可以利用 Cookie 缓存这些帖子、将一些状态添加到网址中或采用其他完全不同的方法。目标是找到一个缓存解决方案,在当前用户向应用发布消息的时间段内为该用户提供数据。请记住,如果在事务内执行 get、祖先查询或任何操作,您会始终看到最新写入的数据。