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?

¿Fue útil?

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/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

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ón manage.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 filtro WHERE 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)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top