Condiciones de carrera en django
-
06-07-2019 - |
Pregunta
Aquí hay un ejemplo simple de una vista de django con una posible condición de carrera:
# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points
def add_points(request):
user = request.user
user.points += calculate_points(user)
user.save()
La condición de carrera debería ser bastante obvia: un usuario puede realizar esta solicitud dos veces, y la aplicación podría ejecutar user = request.user
simultáneamente, haciendo que una de las solicitudes anule a la otra.
Suponga que la función Calculate_points
es relativamente complicada y realiza cálculos basados ??en todo tipo de cosas extrañas que no se pueden colocar en una única update
y que serían difíciles de poner un procedimiento almacenado.
Entonces, esta es mi pregunta: ¿qué tipo de mecanismos de bloqueo están disponibles para django, para hacer frente a situaciones similares a esta?
Solución
Django 1.4+ admite select_for_update , en versiones anteriores puede ejecutar consultas SQL sin procesar, por ejemplo select ... for update
que según la base de datos subyacente bloqueará la fila de cualquier actualización, puede hacer lo que quiera con esa fila hasta el final de la transacción. por ejemplo,
from django.db import transaction
@transaction.commit_manually()
def add_points(request):
user = User.objects.select_for_update().get(id=request.user.id)
# you can go back at this point if something is not right
if user.points > 1000:
# too many points
return
user.points += calculate_points(user)
user.save()
transaction.commit()
Otros consejos
A partir de Django 1.1, puede usar las expresiones F () de ORM para resolver este problema específico.
from django.db.models import F
user = request.user
user.points = F('points') + calculate_points(user)
user.save()
Para más detalles, consulte la documentación:
https: //docs.djangoproject .com / es / 1.8 / ref / models / expressions / # django.db.models.F
El bloqueo de la base de datos es el camino a seguir aquí. Hay planes para agregar " seleccionar para actualización " soporte para Django ( aquí ), pero por ahora lo más simple sería usar SQL sin formato para ACTUALIZAR el objeto del usuario antes de comenzar a calcular la puntuación.
El bloqueo pesimista ahora es compatible con el ORM de Django 1.4 cuando el DB subyacente (como Postgres) lo admite. Consulte las notas de la versión de Django 1.4a1 .
Tiene muchas formas de enhebrar este tipo de cosas.
Un enfoque estándar es Actualizar primero . Haces una actualización que tomará un bloqueo exclusivo en la fila; entonces haz tu trabajo; y finalmente cometer el cambio. Para que esto funcione, debe omitir el almacenamiento en caché del ORM.
Otro enfoque estándar es tener un servidor de aplicaciones de un solo subproceso que aísle las transacciones web del cálculo complejo.
-
Su aplicación web puede crear una cola de solicitudes de puntuación, generar un proceso separado y luego escribir las solicitudes de puntuación en esta cola. El engendro se puede poner en el
urls.py
de Django para que ocurra en el inicio de la aplicación web. O se puede poner en una secuencia de comandos de administraciónmanage.py
separada. O puede hacerse "según sea necesario" cuando se intenta la primera solicitud de puntuación. -
También puede crear un servidor web con sabor WSGI por separado utilizando Werkzeug que acepta solicitudes WS a través de urllib2. Si tiene un solo número de puerto para este servidor, TCP / IP pone en cola las solicitudes. Si su controlador WSGI tiene un subproceso, entonces, ha logrado un subproceso único serializado. Esto es un poco más escalable, ya que el motor de puntuación es una solicitud de WS y se puede ejecutar en cualquier lugar.
Otro enfoque más es tener algún otro recurso que deba adquirirse y mantenerse para hacer el cálculo.
-
Un objeto Singleton en la base de datos. Una sola fila en una tabla única se puede actualizar con una ID de sesión para tomar el control; actualice con el ID de sesión de
None
para liberar el control. La actualización esencial tiene que incluir un filtroWHERE SESSION_ID IS NONE
para garantizar que la actualización falla cuando otra persona retiene el bloqueo. Esto es interesante porque es inherentemente libre de carreras, es una actualización única, no una secuencia SELECCIONAR-ACTUALIZACIÓN. -
Se puede usar un semáforo de variedad de jardín fuera de la base de datos. Las colas (generalmente) son más fáciles de trabajar que un semáforo de bajo nivel.
Esto puede simplificar demasiado su situación, pero ¿qué pasa con un reemplazo de enlace de JavaScript? En otras palabras, cuando el usuario hace clic en el enlace o botón, ajusta la solicitud en una función de JavaScript que inmediatamente deshabilita / "atenúa". el enlace y reemplaza el texto con " Cargando ... " o " Enviando solicitud ... " información o algo similar. ¿Eso funcionaría para ti?
Ahora, debe usar:
Model.objects.select_for_update().get(foo=bar)