2009-06-23 19 views
29

Aquí es un simple ejemplo de una vista de Django con una condición potencial de carrera:Las condiciones de carrera en Django

# 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 debe ser bastante obvia: Un usuario puede hacer esta solicitud dos veces, y la solicitud puedan ejecute user = request.user simultáneamente, provocando que una de las solicitudes sobrescriba a la otra.

Supongamos que la función calculate_points es relativamente complicada, y hace cálculos basados ​​en todo tipo de cosas raras que no se pueden colocar en un solo update y sería difícil ponerlos en un procedimiento almacenado.

Así que aquí está mi pregunta: ¿Qué tipo de mecanismos de bloqueo están disponibles para django, para hacer frente a situaciones similares a esta?

+0

En la primera pasada, parece que necesita bloqueo a nivel de base de datos en la fila en cuestión en ese punto. Consultaría la documentación de SQL para su base de datos y enviaría una consulta personalizada para hacerlo. –

+1

Preferiría una solución "agnóstica de base de datos" si es posible. – Fragsworth

+1

'@ transaction.commit_on_success' +' QuerySet.select_for_update() ' – orokusaki

Respuesta

38

Django 1.4+ apoya select_for_update, en versiones anteriores es posible ejecutar consultas SQL primas, por ejemplo, select ... for update que según el DB subyacente bloqueará la fila de cualquier actualización, puede hacer lo que quiera con esa fila hasta el final de la transacción. p.ej.

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() 
+1

Esta debería ser la respuesta aceptada. –

+0

Parece que hubo un parche durante mucho tiempo para esta función https://code.djangoproject.com/ticket/2705 - Hace poco lo apliqué a Django 1.3.5 (para un proyecto grande, que es difícil migrar a 1.4) – HighCat

+0

Me pregunto cómo esto se implementa mejor como un método de la clase de usuario (para ser reutilizable en otros lugares, no solo en esa vista).El problema para mí es que el código de llamada aún debe hacer la llamada select_for_update(), pero me gustaría que esté encapsulado en el método del usuario. –

6

Tiene muchas formas de enhebrar este tipo de cosas.

Un enfoque estándar es Update First. Usted hace una actualización que aprovechará 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 separado, de un único subproceso que aísla 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 para esta cola. El engendro se puede poner en Django's urls.py para que ocurra en el inicio de la aplicación web. O puede colocarse en una secuencia de comandos de administración manage.py. O puede hacerse "según sea necesario" cuando se intenta la primera solicitud de puntuación.

  • También puede crear un servidor web WSGI separado utilizando Werkzeug que acepte solicitudes de WS a través de urllib2. Si tiene un número de puerto único para este servidor, las solicitudes están en cola por TCP/IP. Si su manejador WSGI tiene un hilo, entonces, ha logrado un enhebrado simple serializado. Esto es ligeramente 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 haya que adquirir y mantener 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; actualizar con ID de sesión de None para liberar el control. La actualización esencial debe incluir un filtro WHERE SESSION_ID IS NONE para garantizar que la actualización falla cuando el bloqueo está en manos de otra persona. Esto es interesante porque es inherentemente libre de raza, es una actualización única, no una secuencia de SELECCIÓN DE ACTUALIZACIÓN.

  • Un semáforo garden-variety se puede utilizar fuera de la base de datos. Las colas (generalmente) son más fáciles de trabajar que un semáforo de bajo nivel.

+1

+1 Me gusta mucho la idea de la cola de solicitud de puntuación. –

+0

Gran respuesta. De alguna manera, el acceso a la fila de la base de datos debe ser serializado y creo que las colas son más escalables que los bloqueos. @Fragsworth: vea este proyecto para una implementación simple de las colas en Django que usa RabbitMQ: http://ask.github.com/celery/introduction.html –

8

El bloqueo de la base de datos es el camino a seguir aquí. Hay planes para agregar compatibilidad con "seleccionar para la actualización" a Django (here), 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.


Bloqueo pesimista está ahora apoyado por ORM de Django 1.4 cuando el DB subyacente (como Postgres) soporta. Vea el Django 1.4a1 release notes.

1

Esto puede estar simplificando demasiado su situación, pero ¿qué pasa con el reemplazo de un enlace de JavaScript? En otras palabras, cuando el usuario hace clic en el enlace o botón, envuelve la solicitud en una función de JavaScript que inmediatamente deshabilita/"pone gris" el enlace y reemplaza el texto con la información "Cargando ..." o "Enviar solicitud ..." o algo similar. ¿Eso funcionaría para ti?

+2

-1, pero aún no protege el sitio. cada cierto tiempo los usuarios están utilizando otros clientes http que navegadores. es decir, el usuario puede usar wget para obtener una URL determinada, y luego, deshabilitar la URL mediante jscript no lo salvará. Jscript se debe usar solo para hacer que el usuario de la página sea más simple si lo desea, pero no debe usarlo para solucionar problemas en la aplicación del lado del servidor. – SashaN

+0

@SashaN: El cartel no decía que esto no solo se accedería a través de un navegador web. No podemos asumir de inmediato todos los demás casos excepcionales como wget. También he prefijado la respuesta con "Esto puede simplificar demasiado tu situación ..." para cubrir los casos de excepción, ya que esta sugerencia puede ser una solución adecuada para muchos. Piense también en los futuros televidentes de esta pregunta que pueden tener un escenario ligeramente diferente en el que esta respuesta podría ser solo el boleto. Ciertamente, no acepto que merezca un voto "no útil", pero sí le agradezco al menos proporcionar una razón. –

+1

"No confiará en el lado del cliente" – Ekevoo

14

A partir de Django 1.1 puede utilizar 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 ver la documentación:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

+4

Las expresiones 'F()' aún no le permiten agregar un condicional a la actualización. Entonces, podría decir aumentar los puntos de los usuarios si aún están activos. –

+0

no ... ¡esto no funcionaría si tienes una actualización dentro de un ciclo for! – NoobEditor

0

Ahora, debe utilizar:

Model.objects.select_for_update().get(foo=bar) 
+1

Una explicación de su intención sería mejorar su respuesta . – reporter

Cuestiones relacionadas