強整合性に対応するデータ構造

Datastore は、多数のマシン間でデータを分散し、同期レプリケーションを広範な地理的領域で使用することで、高可用性、スケーラビリティ、耐久性を実現します。ただし、この設計では書き込みスループットが制限され、1 つのエンティティ グループに対する commit が 1 秒につき 1 回程度までとなります。また、複数のエンティティ グループにまたがるクエリやトランザクションに対しても制限があります。このページではこうした制限事項について詳しく説明します。また、アプリケーションの書き込みスループットに関する要件を満たしつつも、強整合性をサポートするようにデータを構築するためのおすすめの方法を紹介します。

強整合性読み取りは常に現在のデータを返します。トランザクション内で実行された場合は、単一の一貫性のあるスナップショットから得られたように見えます。ただし、クエリは強整合性を維持するために祖先フィルタを指定するか、トランザクションに参加する必要があります。トランザクションには最大で 25 のエンティティ グループを含めることができます。結果整合性読み取りにはこのような制限はなく、多くの場合に適しています。結果整合性読み取りを使用する場合は、大量のエンティティ グループ間でデータを分散できます。この結果、異なるエンティティ グループで commit を並列実行することで、書き込みスループットが向上します。ただし結果整合性読み取りがアプリケーションに適するかどうかを判断するには、その特性を理解しておく必要があります。

  • 結果整合性読み取りによる結果は、最新のトランザクションを反映していない場合があります。このタイプの読み取りでは、読み取りを実行しているレプリカが最新であることが保証されないためです。代わりに結果整合性読み取りでは、クエリの実行時にこのレプリカで使用可能な任意のデータが使用されます。ほとんどの場合、レプリケーション レイテンシは数秒未満です。
  • 複数のエンティティを対象として commit されたトランザクションは、すべてではなく一部のエンティティにしか適用されていないように見える場合があります。しかし、1 つのトランザクションが 1 つのエンティティ内で部分的に適用されたように見えることはありません。
  • クエリ結果には、フィルタ条件に適合しないはずのエンティティが含まれる場合や、フィルタ条件に適合するはずのエンティティが除外される場合があります。これは、インデックスが読み取られるバージョンとエンティティ自体が読み取られるバージョンが異なる場合があるためです。

強整合性に対応するデータ構造化の方法を理解するため、単純なゲストブック アプリケーションを作成する 2 つの方法を比較してみます。最初の方法では、作成された各エンティティに対して新しいルート エンティティを作成します。

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 でクエリを実行し、直近 10 件のグリーティングを求めます。

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));
}

しかし、非祖先クエリを使用しているため、このスキームでのクエリの実行に使用されているレプリカは、クエリが実行されるまで新しいグリーティングを見ていない可能性があります。それでも commit の数秒以内であれば、ほぼすべての書き込みが非祖先クエリで使用できます。多くのアプリケーションでは、現在のユーザー自身による変更を処理する場合、非祖先クエリによる結果を提供するソリューションで通常は十分であり、この程度のレプリケーション レイテンシは完全に許容範囲となります。

アプリケーションにとって強整合性が重要である場合、単一の強整合性祖先クエリで読み取る必要があるすべてのエンティティ間で同じルート エンティティを識別する祖先パスを使用してエンティティを書き込むという代替方法があります。

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 つのゲストブックにつき 1 つのエンティティ グループに書き込むことで、強整合性を実現できます。ただしゲストブックへの変更は、1 秒あたり 1 回のみの書き込みに制限されます(エンティティ グループに対してサポートされる制限)。アプリケーションの書き込み使用量が増えると予想される場合は、他の方法を検討する必要があります。たとえば、最新の投稿は有効期限付の Memcache に入れ、Memcache と Datastore から最新の投稿を組み合わせて表示する、Cookie に投稿をキャッシュする、URL など他のものに完全に状態を入れるなどです。現在のユーザーがアプリケーションに投稿している間に、このユーザーにデータを提供するキャッシュ ソリューションを見つけることが目標となります。トランザクション内で get、祖先クエリ、またはなんらかのオペレーションを実行する場合は、常に最新の書き込みデータが表示されることに留意してください。