2009-10-21 46 views
39

Estoy tratando de incrementar atómicamente un contador simple en Django. Mi código es el siguiente:Incremento atómico de un contador en django

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 Django correctamente, esto debe envolver la función en una transacción y que el incremento atómica. 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?

+0

¿Qué base de datos está utilizando? –

+0

Para mí, parece un desperdicio no usar '+ =' para evitar las condiciones de carrera. Los usuarios de Python ya deberían saber que existe una diferencia entre 'a + = b' y' a = a + b', entonces ¿por qué no usar eso? Tal vez va a entrar en conflicto con algunos datos de caché? No es seguro. – aliqandil

Respuesta

62

New in Django 1.1

Counter.objects.get_or_create(name = name) 
Counter.objects.filter(name = name).update(count = F('count')+1) 

o el uso de an F expression:

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

¿debería estar esto envuelto en un método commit_on_success? – alexef

+6

Un problema con esto es si necesita el valor actualizado después, necesita recuperarlo de la base de datos. En ciertos casos, como la generación de ID, esto puede causar condiciones de carrera. Por ejemplo, dos subprocesos pueden incrementar una ID atómicamente (digamos del 1 al 3), pero luego ambos consultan por el valor actual y obtienen 3, intentan insertar, explotar ... Solo hay algo en lo que pensar. – Bialecki

+0

En la segunda versión, ¿por qué no usar los valores predeterminados kwarg para get_or_create, y luego poner el objeto F dentro de un bloque 'if created'? Debería ser más rápido en el caso de la creación, ¿verdad? Seguí adelante y puse una respuesta demostrando lo que quiero decir. – mlissner

-3

O si lo que desea es un contador y no un objeto persistente puede utilizar itertools contador que está implementado en C. El GIL proporcionará la seguridad necesaria

--Sai

+0

El asker preguntaba específicamente cómo incrementar atómicamente un campo en la base de datos. – slacy

+0

Ditto comentario de slacy –

+0

En la defensa de este tipo, eso no se afirma explícitamente. Claramente intencionado sin embargo. –

14

En Django 1.4 existe support for SELECT ... FOR UPDATE cláusulas, el uso de bloqueos de base de datos para asegurarse de que no hay datos de accesos simultáneamente por error.

+0

Esta fue la solución que terminé combinando con el bloqueo del paquete en la transacción.commit_on_success. – Bialecki

4

Manteniendo la sencillez 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, usted no tiene que hacer más actualizaciones.

5

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

Si no es necesario conocer el valor del contador cuando se establece que, la respuesta más común es sin duda la 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 para agregar 1 al valor de count, que puede funcionar perfectamente sin bloquear otras operaciones. El inconveniente es que no tiene forma de saber qué count 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 la cuenta en este momento, puedes usar la opción select_for_update a la que hace 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 the docs para obtener más información sobre select_for_update.

+0

Esta respuesta tiene la mejor explicación. No fue hasta la lectura de este que estaba convencido de que 'count = F ('count') + 1' funcionaría –

Cuestiones relacionadas