2008-11-11 33 views
24

Estoy tratando de poner en práctica (lo que creo que es) un modelo de datos bastante simple para un contador:Operaciones atómicas en Django?

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 

Cuando alguien viene a través, se buscará una fila que coincide con el visitType y visitDate; si esta fila no existe, se creará con counter = 0.

Luego incrementamos el contador y lo guardamos.

Mi preocupación es que este proceso es totalmente una carrera. Dos solicitudes podrían verificar simultáneamente para ver si la entidad está allí, y ambas podrían crearla. Entre leer el contador y guardar el resultado, podría surgir otra solicitud e incrementarla (lo que daría como resultado una cuenta perdida).

Hasta ahora no he encontrado una buena manera de evitar esto, ya sea en la documentación de Django o en el tutorial (de hecho, parece que el tutorial tiene una condición de carrera en la parte de Votación).

¿Cómo puedo hacer esto de forma segura?

Respuesta

1

Esto es un poco hackeo. El SQL sin procesar hará que su código sea menos portátil, pero eliminará la condición de carrera en el incremento del contador. En teoría, esto debería incrementar el contador cada vez que haga una consulta. No lo he probado, por lo que debes asegurarte de que la lista se interpola correctamente en la consulta.

class VisitorDayTypeCounterManager(models.Manager): 
    def get_query_set(self): 
     qs = super(VisitorDayTypeCounterManager, self).get_query_set() 

     from django.db import connection 
     cursor = connection.cursor() 

     pk_list = qs.values_list('id', flat=True) 
     cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list]) 

     return qs 

class VisitorDayTypeCounter(models.Model): 
    ... 

    objects = VisitorDayTypeCounterManager() 
+0

¿Todavía no es posible que la base de datos ejecute esta consulta en dos conexiones separadas al mismo tiempo y todavía tenga una condición de carrera (con una probabilidad mucho menor)?Todo depende de las transacciones ocultas alrededor de esta consulta implimented por la capa de conexión que hacen op atomic. –

+0

Si ve la nota clave "Por qué odio Django" de DjangoCon, este tipo de consulta se da como una forma correcta y libre de condiciones de carrera para hacer un incremento en SQL (el problema es que el ORM de Django no puede hacerlo por usted)) – iconoplast

+0

Voy a ver tus diapositivas ... has confirmado mi sospecha de que el ORM no lo haría por sí solo. ¡Gracias por la ayuda! –

5

Dos sugerencias:

Añadir un unique_together a su modelo, y envolver la creación de un manejador de excepciones para atrapar duplicados:

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 
    class Meta: 
     unique_together = (('visitType', 'visitDate')) 

Después de esto, se podía stlll tener una condición de carrera menor en el actualización del contador Si obtiene suficiente tráfico como para preocuparse por eso, le sugiero que investigue las transacciones para obtener un control más detallado de la base de datos. No creo que el ORM tenga soporte directo para bloqueo/sincronización. La documentación de la transacción está disponible here.

+0

El unique_together sin duda me hace sentir un poco más cómodo. Probablemente, nunca habrá suficiente tráfico para hacer que la carrera sea atacada, pero como estoy aprendiendo Django al mismo tiempo, pensé que quería "hacerlo bien". ¡Gracias por la ayuda! –

+0

Sí, te escucho. Tal vez alguien más esté al tanto de una función ORM para manejar esto, o puede aclarar si algunos de los integradores son seguros contra este escenario. –

1

¿Por qué no utilizar la base de datos como capa de simultaneidad? Agregue una clave principal o restricción única a la tabla para visitarType y fecha de visita. Si no me equivoco, django no lo soporta exactamente en su clase de Modelo de base de datos o al menos no he visto un ejemplo.

vez que haya añadido la restricción/clave para la tabla, entonces todo lo que tiene que hacer es:

  1. de verificación si la fila está allí. si es así, tómalo.
  2. inserta la fila. si no hay ningún error, estás bien y puedes seguir adelante.
  3. si hay un error (es decir, condición de carrera), recupera la fila. si no hay fila, entonces es un error genuino. De lo contrario, estás bien.

Es desagradable hacerlo de esta manera, pero parece lo suficientemente rápido y cubriría la mayoría de las situaciones.

+0

No maneja el caso donde dos personas van a actualizar el contador al mismo tiempo. –

0

Debe utilizar las transacciones de la base de datos para evitar este tipo de condición de carrera. Una transacción le permite realizar toda la operación de crear, leer, incrementar y guardar el contador en una base de "todo o nada". Si algo sale mal, hará que todo vuelva a la normalidad y podrás volver a intentarlo.

Eche un vistazo a Django docs. Hay un middleware de transacción, o puede usar decoradores alrededor de vistas o métodos para crear transacciones.

+0

Acepto que las transacciones parecen ser la respuesta aquí, pero no está claro que la funcionalidad realmente resuelva el problema de incremento: el SELECCIONAR para obtener la fila seguirá teniendo éxito y la ACTUALIZACIÓN para cambiar el valor del contador seguirá teniendo éxito. Si estoy equivocado, un ejemplo sería increíble. –

+0

Debería bloquear la tabla durante la selección para hacerlo de esta manera, y tal como lo menciona Sam, eso reduciría su rendimiento. Esta es la mejor manera si no incrimen el mostrador con frecuencia. –

12

Si realmente quiere que el contador sea preciso, podría usar una transacción, pero la cantidad de concurrencia requerida realmente arrastrará su aplicación y base de datos hacia abajo bajo cualquier carga significativa.En su lugar, piense en utilizar un enfoque de estilo de mensajería más y simplemente siga volcando los registros de recuento en una tabla para cada visita en la que desee aumentar el contador. Luego, cuando desee el número total de visitas, haga un recuento en la tabla de visitas. También podría tener un proceso en segundo plano que se ejecute varias veces al día que sume las visitas y luego almacenarlo en la tabla principal. Para ahorrar espacio también borrará cualquier registro de la tabla de visitas secundarias que haya resumido. Reducirá sus costos de concurrencia una cantidad enorme si no tiene varios agentes compitiendo por los mismos recursos (el contador).

+0

¡Hola, buena llamada! He estado haciendo el trabajo de App Engine principalmente, y me he colgado de "las transacciones solo actúan en una entrada" y "hacer funciones agregadas es muy caro". Esa es una forma realmente simple de resolver el problema. ¡Gracias! –

+0

Supongo que depende de si el proceso va a ser de lectura pesada o de escritura pesada. Los recuentos se leerán con mucha más frecuencia de lo que se incrementarán en mi sistema, por lo que para el problema tal como se indicó, este podría no ser el mejor plan. Sin embargo, resuelve otras preocupaciones que tenía, ¡así que gracias! –

+0

Según la antigüedad de los recuentos, puede tener un proceso en segundo plano resumiéndolos cada cierto tiempo. Entonces no harías la agregación por solicitud. –

6

Puede usar el parche desde http://code.djangoproject.com/ticket/2705 para el bloqueo del nivel de base de datos de soporte.

Con el parche este código será atómica:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update() 
visitors.counter += 1 
visitors.save() 
+0

Eso es genial. No vi eso cuando hice la pregunta por primera vez (¡hace 3 años!) –

26

A partir de Django 1.1 puede utilizar expresiones del ORM F().

from django.db.models import F 
product = Product.objects.get(name='Venezuelan Beaver Cheese') 
product.number_sold = F('number_sold') + 1 
product.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

+0

¡Genial! Ese es un enfoque increíble. Si solo hubiera estado allí cuando estaba trabajando en este proyecto. –

+0

Muy bien, ¡gracias por la información! –

+0

Para las instalaciones modernas de Django, esta es la respuesta correcta y debe ser reflejada como tal por el OP. – claymation

Cuestiones relacionadas