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.
Counter.objects.get_or_create(name = name)
Counter.objects.filter(name = name).update(count = F(''count'')+1)
o usando una expresión F :
counter = Counter.objects.get_or_create(name = name)
counter.count = F(''count'') +1
counter.save()
Se ha agregado un tema sobre la condición de carrera asociada con este enfoque a la documentación oficial.