Aislamiento de transacción en App Engine

Max Ross

Según Wikipedia, el nivel de aislamiento de un sistema de administración de bases de datos “define cómo y cuándo los cambios realizados por una operación se vuelven visibles para otras operaciones simultáneas”. El objetivo de este artículo es explicar el aislamiento de consultas y transacciones en Cloud Datastore que usa App Engine. Después de leer este artículo, comprenderás mejor cómo se comportan las operaciones de lectura y escritura simultáneas dentro de las transacciones y fuera de ellas.

Dentro de las transacciones: nivel serializable

En orden de mayor a menor, estos son los cuatro niveles de aislamiento: serializable, de lectura repetible, de lectura confirmada y de lectura no confirmada. Las transacciones de Datastore cumplen con el nivel de aislamiento serializable. Cada transacción está aislada por completo de todas las demás transacciones y operaciones del almacén de datos. Las transacciones en un grupo de entidades determinado se ejecutan en serie, una después de la otra.

Consulta la sección Aislamiento y coherencia de la documentación sobre transacciones para obtener más información, y el artículo de Wikipedia acerca del aislamiento de instantáneas.

Fuera de las transacciones: nivel de lectura confirmada

Las operaciones de Datastore fuera de las transacciones se asemejan más al nivel de aislamiento de lectura confirmada. Las entidades recuperadas del almacén de datos por consultas u operaciones GET solo verán los datos confirmados. Una entidad recuperada nunca tendrá datos confirmados de forma parcial (algunos de antes de una confirmación y algunos de después). Sin embargo, la interacción entre las consultas y las transacciones es un poco más sutil y, para comprenderla, debemos analizar el proceso de confirmación de forma más detallada.

El proceso de confirmación

Cuando una confirmación se muestra de forma correcta, se garantiza la aplicación de la transacción, pero eso no significa que el resultado de la escritura resulte visible de forma inmediata para los lectores. La aplicación de una transacción consiste en dos eventos importantes:

  • Evento A: Es el punto en el que se aplicaron los cambios a una entidad.
  • Evento B: Es el punto en el que se aplicaron los cambios a los índices de esa entidad.

Muestra las flechas de progreso desde la transacción de confirmación hasta los cambios de entidad visibles para los cambios de índices y entidades visibles.

En Cloud Datastore, la transacción suele aplicarse completamente en unos cientos de milisegundos después de que se muestra la confirmación. Sin embargo, incluso si no se aplica por completo, las lecturas, las escrituras y las consultas principales posteriores siempre reflejarán los resultados de la confirmación, ya que se aplicarán las modificaciones pendientes antes de su ejecución. Sin embargo, las consultas que abarcan varios grupos de entidades no pueden determinar si hay modificaciones pendientes antes de la ejecución y pueden mostrar resultados obsoletos o parcialmente aplicados.

Se garantiza que una solicitud que busca una entidad actualizada por su clave en un momento posterior al evento A vea la última versión de esa entidad. Sin embargo, si una solicitud simultánea ejecuta una consulta y la entidad previa a la actualización no cumple con el predicado (la cláusula WHERE, para los entusiastas de SQL/GQL), sino que lo hace la entidad posterior a la actualización, la entidad será parte del conjunto de resultados solo si la consulta se ejecuta después de que la operación de aplicar alcance el evento B.

En otras palabras, durante las ventanas breves, es posible que un conjunto de resultados no incluya una entidad cuyas propiedades cumplan con el predicado de la consulta, según el resultado de una búsqueda por clave. También es posible que un conjunto de resultados incluya una entidad cuyas propiedades, de nuevo según el resultado de una búsqueda por clave, no cumplan con el predicado de la consulta. Una consulta no puede tener en cuenta las transacciones que se encuentran entre el evento A y el B para decidir qué entidades mostrar. Se realizará con datos obsoletos; sin embargo, si realizas una operación get() en las claves del resultado, siempre obtendrás la última versión de esa entidad. Esto significa que es posible que falten resultados que coincidan con la consulta o que obtengas resultados que no coincidan una vez que obtengas la entidad correspondiente.

Existen situaciones en las que se garantiza que todas las modificaciones pendientes se apliquen completamente antes de que se ejecute la consulta, como cualquier consulta principal en Cloud Datastore. En este caso, los resultados de la consulta siempre serán actuales y coherentes.

Ejemplos

Ya tienes una explicación general de cómo interactúan las actualizaciones y las consultas simultáneas. Si embargo, si te pareces a mí, seguro te resulta más fácil comprender estos conceptos mediante ejemplos concretos. Veamos algunos. Comenzaremos con algunos ejemplos sencillos y terminaremos con los más interesantes.

Supongamos que tenemos una aplicación que almacena entidades que llamaremos Persona. Una entidad Persona tiene las siguientes propiedades:

  • Nombre
  • Altura

Esta aplicación admite las siguientes operaciones:

  • updatePerson()
  • getTallPeople(), que muestra todas las personas de más de 1.80 metros de alto.

Tenemos dos entidades Person en el almacén de datos:

  • Juan, que mide 1.72 metros de alto.
  • Pedro, que mide 1.85 metros de alto.

Ejemplo 1: Hagamos más alto a Juan

Supongamos que una aplicación recibe dos solicitudes prácticamente al mismo tiempo. La primera solicitud actualiza la altura de Juan de 1.72 a 1.87 metros. ¡Un gran estirón! La segunda solicitud llama a getTallPeople(). ¿Qué muestra getTallPeople()?

La respuesta depende de la relación entre los dos eventos de confirmación activados por la solicitud 1 y la consulta getTallPeople() que ejecuta la solicitud 2. Supongamos que se ve así:

  • Solicitud 1, put()
  • Solicitud 2, getTallPeople()
  • Solicitud 1, put()-->commit()
  • Solicitud 1, put()-->commit()-->evento A
  • Solicitud 1, put()-->commit()-->evento B

En este caso, getTallPeople() solo mostrará a Pedro. ¿Por qué? Debido a que la actualización de Juan que aumenta su altura todavía no se confirmó, el cambio aún no es visible para la consulta que hicimos en la solicitud 2.

Ahora, supongamos que se ve así:

  • Solicitud 1, put()
  • Solicitud 1, put()-->commit()
  • Solicitud 1, put()-->commit()-->evento A
  • Solicitud 2, getTallPeople()
  • Solicitud 1, put()-->commit()-->evento B

En este caso, la consulta se ejecuta antes de que la solicitud 1 alcance el evento B, por lo que las actualizaciones de los índices de Persona aún no se aplicaron. Como resultado, getTallPeople() solo muestra a Pedro. Este es un ejemplo de un conjunto de resultados que excluye una entidad cuyas propiedades cumplen con el predicado de la consulta.

Ejemplo 2: Hagamos a Pedro más pequeño (lo sentimos, Pedro)

En este ejemplo, la solicitud 1 hará algo diferente. En lugar de aumentar la altura de Juan de 1.72 a 1.87 metros, se reducirá la altura de Pedro de 1.85 a 1.65 metros. Una vez más, ¿qué muestra getTallPeople()

return?
  • Solicitud 1, put()
  • Solicitud 2, getTallPeople()
  • Solicitud 1, put()-->commit()
  • Solicitud 1, put()-->commit()-->evento A
  • Solicitud 1, put()-->commit()-->evento B

En este caso, getTallPeople() solo mostrará a Pedro. ¿Por qué? Debido a que la actualización de Pedro que reduce su altura todavía no se confirmó, el cambio aún no es visible para la consulta que emitimos en la solicitud 2.

Ahora, supongamos que se ve así:

  • Solicitud 1, put()
  • Solicitud 1, put()-->commit()
  • Solicitud 1, put()-->commit()-->evento A
  • Solicitud 1, put()-->commit()-->evento B
  • Solicitud 2, getTallPeople()

En este caso, getTallPeople() no mostrará a nadie. ¿Por qué? Porque la actualización a Pedro que disminuye su altura ya se había confirmado en el momento en que hicimos nuestra consulta en la solicitud 2.

Ahora, supongamos que se ve así:

  • Solicitud 1, put()
  • Solicitud 1, put()-->commit()
  • Solicitud 1, put()-->commit()-->evento A
  • Solicitud 2, getTallPeople()
  • Solicitud 1, put()-->commit()-->evento B

En este caso, la consulta se ejecuta antes del evento B, por lo que las actualizaciones de los índices de Persona aún no se aplicaron. Como resultado, getTallPeople() sigue mostrando a Pedro, pero la propiedad de altura de la entidad Persona que se muestra es el valor actualizado: 1.65. Este es un ejemplo de un conjunto de resultados que incluye una entidad cuyas propiedades no cumplen con el predicado de la consulta.

Conclusión

Como puedes ver en los ejemplos anteriores, el nivel de aislamiento de transacciones de Cloud Datastore es bastante cercano al de lectura confirmada. Existen, por supuesto, diferencias significativas, pero ahora que comprendes estas diferencias y las razones detrás de ellas, deberías estar en una mejor posición para tomar decisiones de diseño inteligentes relacionadas con el almacén de datos de tus aplicaciones.