¿Cómo prevenir las condiciones de carrera en una aplicación web?

31

Considere un sitio de comercio electrónico, donde Alice y Bob están editando las listas de productos. Alice está mejorando las descripciones, mientras que Bob está actualizando los precios. Comienzan a editar Acme Wonder Widget al mismo tiempo. Bob termina primero y guarda el producto con el nuevo precio. Alice tarda un poco más en actualizar la descripción, y cuando termina, guarda el producto con su nueva descripción. Desafortunadamente, ella también sobrescribe el precio con el precio anterior, que no estaba previsto.

En mi experiencia, estos problemas son extremadamente comunes en las aplicaciones web. Algunos programas (p. Ej., Software wiki) tienen protección contra esto; por lo general, el segundo guardado falla con "la página se actualizó mientras estaba editando". Pero la mayoría de los sitios web no tienen esta protección.

Vale la pena señalar que los métodos del controlador son seguros para subprocesos en sí mismos. Usualmente usan transacciones de bases de datos, lo que las hace seguras en el sentido de que si Alice y Bob intentan ahorrar en el mismo momento preciso, no causarán corrupción. La condición de carrera surge de que Alice o Bob tienen datos obsoletos en su navegador.

¿Cómo podemos prevenir tales condiciones de carrera? En particular, me gustaría saber:

  • ¿Qué técnicas se pueden usar? por ejemplo, rastrear el tiempo del último cambio Cuáles son los pros y los contras de cada uno.
  • ¿Qué es una experiencia útil para el usuario?
  • ¿En qué marcos se ha incorporado esta protección?
paj28
fuente
Ya ha dado la respuesta: rastreando la fecha de cambio de objetos y comparándola con la antigüedad de los datos que otros cambios intentan actualizar. ¿Quieres saber algo más, por ejemplo, cómo hacerlo de manera eficiente?
Kilian Foth
@KilianFoth - He agregado información sobre lo que me gustaría saber en particular
paj28
1
Su pregunta no es especial para las aplicaciones web, las aplicaciones de escritorio pueden tener exactamente el mismo problema. Las estrategias de solución típicas se describen aquí: stackoverflow.com/questions/129329/…
Doc Brown
2
Para su información, la forma de bloqueo que menciona en su pregunta se conoce como " control de concurrencia optimista "
TehShrike
Algunas discusiones relacionadas con Django aquí
paj28

Respuestas:

17

Debe "leer sus escritos", lo que significa que antes de escribir un cambio, debe leer el registro nuevamente y verificar si se realizaron cambios desde la última vez que lo leyó. Puede hacer esto campo por campo (de grano fino) o en base a una marca de tiempo (de grano grueso). Mientras realiza esta comprobación, necesita un bloqueo exclusivo en el registro. Si no se realizaron cambios, puede escribir sus cambios y liberar el bloqueo. Si el registro ha cambiado mientras tanto, cancela la transacción, libera el bloqueo y notifica al usuario.

Phil
fuente
Esto suena como el enfoque más práctico. ¿Conoces algún marco que implemente esto? Creo que el mayor problema con este esquema es que un simple mensaje de "conflicto de edición" frustrará a los usuarios, pero intentar fusionar los conjuntos de cambios (manual o automáticamente) es difícil.
paj28
Desafortunadamente, no conozco ningún framework que soporte esto fuera de la caja. No creo que un mensaje de error de edición de un conflicto se perciba como frustrante, siempre que no sea frecuente. En última instancia, depende de la carga del usuario del sistema, ya sea que solo verifique la marca de tiempo o implemente una función de fusión más compleja.
Phil
Mantuve un producto de base de datos distribuido para PC que utilizaba el enfoque detallado (contra su copia de la base de datos local): si un usuario cambiaba el precio y el otro cambiaba la descripción, ¡no hay problema! Como en la vida real. Si dos usuarios cambiaron el precio, el segundo usuario recibe una disculpa e intenta su cambio nuevamente. ¡No hay problema! Esto no requiere bloqueos, excepto durante el momento en que los datos se escriben en la base de datos. No importa si un usuario va a almorzar mientras su cambio está en la pantalla y lo envía más tarde. Para los cambios de bases de datos remotas, se basó en marcas de tiempo de registro.
1
Dataflex tenía una función llamada "reread ()" que hace lo que usted describe. En las versiones posteriores, era seguro en un entorno multiusuario. Y, de hecho, era la única forma de hacer que esas actualizaciones entrelazadas funcionen.
¿Puedes dar un ejemplo de cómo hacer esto con el servidor SQL? \
l --''''''--------- '' '' '' '' '' ''
10

He visto 2 formas principales:

  1. Agregue la marca de tiempo de la última actualización de la página que el uso está editando en una entrada oculta. Al confirmar, la marca de tiempo se compara con la actual y si no coinciden, otra persona la ha actualizado y devuelve un error.

    • pro: múltiples usuarios pueden editar diferentes partes de la página. La página de error puede conducir a una página de diferencias donde el segundo usuario puede fusionar sus cambios en la nueva página.

    • contra: a veces se desperdician grandes partes del esfuerzo durante grandes ediciones simultáneas.

  2. Cuando un usuario comienza a editar la página, bloquéela durante un período de tiempo razonable, cuando otro usuario intente editar, recibirá una página de error y tendrá que esperar hasta que el bloqueo caduque o el primer usuario se haya comprometido.

    • pro: los esfuerzos de edición no se desperdician.

    • con: un usuario sin escrúpulos puede bloquear una página indefinidamente. Una página con un bloqueo caducado aún puede comprometerse a menos que se trate de otra manera (utilizando la técnica 1)

monstruo de trinquete
fuente
7

Utilizar control de simultaneidad optimista .

Agregue una columna versionNumber o versionTimestamp a la tabla en cuestión (el entero es el más seguro).

El usuario 1 lee el registro:

{id:1, status:unimportant, version:5}

El usuario 2 lee el registro:

{id:1, status:unimportant, version:5}

El usuario 1 guarda el registro, esto incrementa la versión:

save {id:1, status:important, version:5}
new value {id:1, status:important, version:6}

El usuario 2 intenta guardar el registro que leen:

save {id:1, status:unimportant, version:5}
ERROR

Hibernate / JPA puede hacer esto automáticamente con el @Version anotación

Debe mantener el estado del registro de lectura en algún lugar, generalmente en sesión (esto es más seguro que en una variable de forma oculta).

Neil McGuigan
fuente
Gracias ... particularmente útil para saber sobre @Version. Una pregunta: ¿por qué es seguro almacenar el estado en la sesión? En ese caso, me preocuparía que usar el botón Atrás podría confundir las cosas.
paj28
La sesión es más segura que un elemento de formulario oculto ya que el usuario no podría cambiar el valor de la versión. Si eso no es una preocupación, entonces ignore la parte sobre la sesión
Neil McGuigan el
Esta técnica se llama bloqueo optimista en línea y es en SQLAlchemy así
paj28
@ paj28: ese enlace SQLAlchemyno apunta a nada sobre bloqueos optimistas fuera de línea, y no puedo encontrarlo en los documentos. ¿Tenía un enlace más útil o simplemente señalaba a las personas en SQLAlchemy en general?
dwanderson
@dwanderson Me refería a la versión contraria de ese enlace.
paj28
1

Algunos sistemas de Mapeo Relacional de Objetos (ORM) detectarán qué campos de un objeto han cambiado desde que se cargaron desde la base de datos, y construirán la declaración de actualización de SQL para establecer solo esos valores. ActiveRecord para Ruby on Rails es uno de esos ORM.

El efecto neto es que los campos que el usuario no cambió no están incluidos en el comando ACTUALIZACIÓN enviado a la base de datos. Las personas que actualizan diferentes campos al mismo tiempo no sobrescriben los cambios de los demás.

Según el lenguaje de programación que esté utilizando, investigue qué ORM están disponibles y vea si alguno de ellos solo actualizará las columnas de la base de datos marcadas como "sucias" en su aplicación.

Greg Burghardt
fuente
Hola Greg. Desafortunadamente, esto no ayuda con este tipo de condiciones de carrera. Si considera mi ejemplo original, cuando Alice guarda, el ORM verá la columna de precios como sucia y la actualizará, aunque no se desee la actualización.
paj28
1
@ paj28 El punto clave en la respuesta de Greg es " campos que el usuario no cambió ". Alice no cambió el precio, por lo que el ORM no intentaría guardar el valor del "precio" en la base de datos.
Ross Patterson
@RossPatterson: ¿cómo sabe el ORM la diferencia entre los campos que cambió el usuario y los datos obsoletos del navegador? No lo hace, al menos sin hacer un seguimiento adicional. Si desea editar la respuesta de Greg para incluir dicho seguimiento, o enviar otra respuesta, sería útil.
paj28
@ paj28 alguna parte del sistema tiene que saber lo que hizo el usuario y solo almacenar los cambios realizados por el usuario. Si el usuario cambió el precio, luego lo volvió a cambiar y luego lo envió, esto no debería contar como "algo que el usuario cambió", porque no lo hizo. Si tiene un sistema que requiere este nivel de control de concurrencia, debe construirlo de esta manera. Si no, no.
@nocomprende - Parte, claro, pero no el ORM como dice esta respuesta
paj28