Structurer des données pour renforcer la cohérence

Pour garantir la haute disponibilité, l'évolutivité et la durabilité, Datastore distribue les données sur de nombreuses machines et met en œuvre une réplication synchrone sur une vaste zone géographique. Cette conception implique toutefois une concession : le débit en écriture d'un seul groupe d'entités donné est limité à environ un commit par seconde, et il existe des restrictions en termes de requêtes ou de transactions couvrant plusieurs groupes d'entités. Cette page décrit plus en détail ces limites et indique les bonnes pratiques à suivre pour structurer les données, afin de garantir une cohérence forte tout en respectant les exigences de débit en écriture de l'application.

Les lectures fortement cohérentes renvoient toujours les données actuelles et, si elles sont effectuées dans une transaction, elles semblent provenir d'un seul instantané cohérent. Toutefois, les requêtes doivent spécifier un filtre d'ancêtre afin d'être fortement cohérentes ou de participer à une transaction, et les transactions peuvent impliquer 25 groupes d'entités au maximum. Les lectures cohérentes à terme ne sont pas soumises à ces limites et conviennent dans de nombreux cas. Avec des lectures cohérentes à terme, vous pouvez répartir les données entre un plus grand nombre de groupes d'entités, ce qui vous permet d'obtenir un meilleur débit en écriture en exécutant des commits en parallèle sur les différents groupes d'entités. Cependant, vous devez connaître les caractéristiques des lectures cohérentes à terme afin de déterminer si elles sont adaptées à votre application :

  • Les résultats de ces lectures peuvent ne pas refléter les dernières transactions. Cela peut se produire, car ces lectures ne garantissent pas que l'instance dupliquée sur laquelle elles s'exécutent est à jour. À la place, elles utilisent les données disponibles sur cette instance dupliquée au moment de l'exécution de la requête, quelles qu'elles soient. La latence de réplication est presque toujours inférieure à quelques secondes.
  • Une transaction ayant fait l'objet d'un commit et qui couvre plusieurs entités peut sembler avoir été appliquée à certaines entités, et non à d'autres. Sachez cependant qu'une transaction ne semblera jamais avoir été partiellement appliquée au sein d'une seule entité.
  • Les résultats de la requête peuvent inclure des entités qui n'auraient pas dû l'être selon les critères de filtrage, et peuvent exclure des entités qui auraient dû être incluses. Cela peut se produire, car les index peuvent être lus avec une version différente de celle employée pour lire l'entité elle-même.

Pour comprendre comment structurer les données de façon à assurer une cohérence forte, nous allons comparer deux approches différentes pour une application simple de livre d'or. La première approche consiste à créer une entité racine dans chaque entité créée :

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

Une requête est ensuite exécutée sur le genre d'entité Greeting pour obtenir les 10 messages d'accueil les plus récents.

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

Toutefois, étant donné que vous utilisez une requête non ascendante, l'instance dupliquée permettant d'exécuter la requête dans ce schéma n'a peut-être pas vu le nouveau message d'accueil au moment de l'exécution de la requête. Néanmoins, presque toutes les écritures sont disponibles pour les requêtes non ascendantes quelques secondes après le commit. Pour de nombreuses applications, une solution fournissant les résultats d'une requête non ascendante dans le contexte des modifications de l'utilisateur actuel est généralement suffisante pour rendre de telles latences de réplication parfaitement acceptables.

Si une cohérence forte est importante pour votre application, une autre approche consiste à écrire des entités avec un chemin d'ancêtre qui identifie la même entité racine dans toutes les entités devant être lues dans une seule requête ascendante fortement cohérente :

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

Vous pouvez ensuite exécuter une requête ascendante fortement cohérente au sein du groupe d'entités identifié par l'entité racine commune :

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

Cette approche assure une cohérence forte en effectuant les opérations d'écriture dans un seul groupe d'entités par livre d'or, mais elle limite également les modifications apportées à ce dernier à une écriture par seconde (limite acceptée pour les groupes d'entités). Si le nombre d'écritures est susceptible d'être plus élevé dans votre application, vous devrez peut-être envisager de recourir à d'autres moyens. Par exemple, vous pouvez placer les posts récents dans Memcache avec une date d'expiration, et afficher une combinaison de posts récents issus de Memcache et de Datastore. Vous avez également la possibilité de les mettre en cache dans un cookie, d'indiquer un état dans l'URL, ou tout autre chose encore. L'objectif est de trouver une solution de mise en cache fournissant les données de l'utilisateur actuel pour la période pendant laquelle celui-ci publie sur l'application. N'oubliez pas que si vous effectuez une opération "get", une requête ascendante ou toute opération dans une transaction, vous verrez toujours les dernières données écrites.