python django transactions race-condition

python - Incremento atómico de un contador en django



transactions race-condition (6)

Estoy tratando de incrementar atómicamente un contador simple en Django. Mi código se ve así:

from models import Counter from django.db import transaction @transaction.commit_on_success def increment_counter(name): counter = Counter.objects.get_or_create(name = name)[0] counter.count += 1 counter.save()

Si entiendo a Django correctamente, esto debería ajustar la función en una transacción y hacer que el incremento sea atómico. Pero no funciona y hay una condición de carrera en la actualización del contador. ¿Cómo se puede hacer que este código sea seguro para subprocesos?


Django 1.7

from django.db.models import F counter, created = Counter.objects.get_or_create(name = name) counter.count = F(''count'') +1 counter.save()


En Django 1.4 hay soporte para cláusulas SELECT ... FOR UPDATE , que utilizan bloqueos de base de datos para garantizar que ningún dato sea accedido simultáneamente por error.


Manteniéndolo simple y basándose en la respuesta de @Oduvan:

counter, created = Counter.objects.get_or_create(name = name, defaults={''count'':1}) if not created: counter.count = F(''count'') +1 counter.save()

La ventaja aquí es que si el objeto fue creado en la primera declaración, no tiene que hacer más actualizaciones.


O si solo quiere un contador y no un objeto persistente, puede usar el contador de itertools que se implementa en C. El GIL proporcionará la seguridad necesaria.

--Sai


Si no necesita saber el valor del contador cuando lo configura, la mejor respuesta es definitivamente su mejor opción:

counter = Counter.objects.get_or_create(name = name) counter.count = F(''count'') + 1 counter.save()

Esto le dice a su base de datos que agregue 1 valor al count , que puede funcionar perfectamente sin bloquear otras operaciones. El inconveniente es que no tiene manera de saber qué cantidad acaba de configurar. Si dos subprocesos aciertan simultáneamente a esta función, ambos verían el mismo valor, y ambos le dirían al db que agregue 1. El db terminaría agregando 2 como se esperaba, pero no sabrá cuál fue el primero.

Si realmente te importa el recuento en este momento, puedes usar la opción select_for_update la que select_for_update referencia Emil Stenstrom. Esto es lo que parece:

from models import Counter from django.db import transaction @transaction.atomic def increment_counter(name): counter = (Counter.objects .select_for_update() .get_or_create(name=name)[0] counter.count += 1 counter.save()

Esto lee el valor actual y bloquea las filas coincidentes hasta el final de la transacción. Ahora solo un trabajador puede leer a la vez. Consulte los documentos para obtener más información sobre select_for_update.