Pregunta

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 aparece alguien, buscará una fila que coincida con visitType y visitDate; Si esta fila no existe, se creará con counter = 0.

Luego incrementamos el contador y guardamos.

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

Hasta ahora no he encontrado una buena forma 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 Voto).

¿Cómo hago esto de manera segura?

¿Fue útil?

Solución

Esto es un poco hack. El SQL sin formato 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 realice una consulta. No he probado esto, por lo que debe asegurarse de que la lista se interpola en la consulta correctamente.

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

Otros consejos

A partir de Django 1.1 puede usar las expresiones F () de ORM.

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, 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

Si realmente desea que el contador sea preciso, puede 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 un enfoque más de estilo de mensajería y simplemente mantenga los registros de conteo en una tabla para cada visita donde desee incrementar el contador. Luego, cuando desee el número total de visitas, haga un recuento en la tabla de visitas. También puede tener un proceso en segundo plano que se ejecute cualquier cantidad de veces al día que sume las visitas y luego lo almacene en la tabla principal. Para ahorrar espacio, también eliminaría todos los registros de la tabla de visitas secundarias que resumió. Reducirá sus costos de concurrencia una gran cantidad si no tiene múltiples agentes compitiendo por los mismos recursos (el contador).

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

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

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

Dos sugerencias:

Agregue un unique_together a su modelo y envuelva la creación en un controlador 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, aún podría tener una condición de carrera menor en la actualización del contador. Si obtiene suficiente tráfico para preocuparse por eso, sugeriría buscar transacciones para un control de base de datos más detallado. No creo que el ORM tenga soporte directo para bloqueo / sincronización. La documentación de la transacción está disponible aquí .

¿Por qué no usar la base de datos como capa de concurrencia? Agregue una clave primaria o restricción única a la tabla para visitType y visitDate. Si no me equivoco, django no es exactamente compatible con esto 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. compruebe si la fila está allí. si es así, tráelo.
  2. inserte la fila. si no hay error, estás bien y puedes seguir adelante.
  3. si hay un error (es decir, condición de carrera), vuelva a buscar la fila. Si no hay una 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.

Debe 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 un "todo o nada". base. Si algo sale mal, retrocederá todo y podrá intentarlo nuevamente.

Consulte los documentos de Django. Hay un intermediario de transacciones, o puede usar decoradores alrededor de vistas o métodos para crear transacciones.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top