transaction error database django concurrency locking race-condition

database - error - Operaciones atómicas en Django?



rollback mongodb (7)

Estoy tratando de implementar (lo que creo que es) un modelo de datos bastante simple para un contador:

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

Cuando alguien llega, buscará una fila que coincida 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, ni en la documentación de Django ni en el tutorial (de hecho, parece que el tutorial tiene una condición de carrera en la parte de Voto).

¿Cómo hago esto de manera segura?


¿Por qué no usar la base de datos como la capa de concurrencia? 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.

Una vez que haya agregado la restricción / clave a la tabla, todo lo que tiene que hacer es:

  1. comprueba 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.


Debes usar 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.

Echa un vistazo a los documentos de Django . Existe un middleware de transacciones, o puede usar decoradores alrededor de vistas o métodos para crear transacciones.


Si realmente desea 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).


Esto es un poco complicado. 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()


Dos sugerencias:

Agregue un unique_together a su modelo y ajuste la creación en un manejador de excepciones para capturar 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, todavía podría tener una condición de carrera menor en la 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 aquí .


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

Con parche, este código será atómico:

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