Domanda Incremento atomico di un contatore in django


Sto cercando di incrementare atomicamente un semplice contatore in Django. Il mio codice assomiglia a questo:

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

Se capisco correttamente Django, questo dovrebbe avvolgere la funzione in una transazione e rendere l'incremento atomico. Ma non funziona e c'è una condizione di competizione nel contro aggiornamento. Come può questo codice essere reso thread-safe?


44
2017-10-21 05:49


origine


risposte:


Novità in Django 1.1

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

o usando un'espressione F:

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

Un argomento sul condizioni di gara associate a questo approccio è stato aggiunto alla documentazione ufficiale.


68
2017-10-21 06:43



In Django 1.4 c'è supporto per SELECT ... FOR UPDATE clausole, utilizzando blocchi di database per assicurarsi che nessun dato sia acceduto in modo concorrente per errore.


14
2018-01-17 21:26



Mantenerlo semplice e basandosi sulla risposta di @ Oduvan:

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

Il vantaggio è che se l'oggetto è stato creato nella prima istruzione, non è necessario eseguire ulteriori aggiornamenti.


7
2017-09-21 20:44



Se non è necessario conoscere il valore del contatore quando lo si imposta, la risposta migliore è sicuramente la soluzione migliore:

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

Questo dice al tuo database di aggiungere 1 al valore di count, che può fare perfettamente senza bloccare altre operazioni. Lo svantaggio è che non hai modo di sapere cosa count hai appena impostato. Se due thread colpiscono simultaneamente questa funzione, entrambi vedrebbero lo stesso valore, e direbbero entrambi al db di aggiungere 1. Il db finirebbe per aggiungere 2 come previsto, ma non si saprà quale sia andato per primo.

Se ti interessa il conteggio adesso, puoi usare il select_for_update opzione a cui fa riferimento Emil Stenstrom. Ecco come appare:

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

Questo legge il valore corrente e blocca le righe corrispondenti fino alla fine della transazione. Ora solo un lavoratore può leggere alla volta. Vedere i documenti per ulteriori informazioni su select_for_update.


7
2017-09-29 19:14



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

5
2018-01-30 03:53



O se vuoi solo un contatore e non un oggetto persistente puoi usare il contatore itertools che è implementato in C. Il GIL fornirà la sicurezza necessaria.

--Sai


-3
2017-08-26 07:33