2009-10-21 16 views
39

Próbuję atomowo zwiększyć prosty licznik w Django. Mój kod wygląda następująco:Atomowy przyrost licznika w 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() 

Jeśli dobrze zrozumiem Django, powinno to zawijać funkcję w transakcji i zwiększać ją atomowo. Ale to nie działa, aw aktualizacji licznika jest stan wyścigu. W jaki sposób ten kod można uczynić wątkiem bezpiecznym?

+0

Jakiej bazy danych używasz? –

+0

Dla mnie wygląda na to, że nie należy używać '+ =', aby uniknąć warunków wyścigu. Użytkownicy Pythona powinni już wiedzieć, że istnieje różnica między "a + = b" i "a = a + b", więc dlaczego nie skorzystać z tego? Może to będzie sprzeczne z niektórymi danymi z pamięci podręcznej? Niepewny. – aliqandil

Odpowiedz

62

New in Django 1.1

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

lub używając an F expression:

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

czy powinien to być zawinięty w metodę commit_on_success? – alexef

+6

Jednym z problemów jest to, że jeśli później potrzebujesz zaktualizowanej wartości, musisz pobrać ją z bazy danych. W niektórych przypadkach, takich jak generowanie identyfikatorów, może to powodować warunki wyścigu. Na przykład, dwa wątki mogą zwiększać liczbę atomową (powiedzmy od 1 do 3), ale wtedy obydwa zapytania dotyczą bieżącej wartości i otrzymują 3, próbują wstawić, eksplozja ... Po prostu coś do przemyślenia. – Bialecki

+0

W drugiej wersji, dlaczego nie użyć wartości domyślnej kwarg do get_or_create, a następnie umieścić obiekt F w bloku 'if created'? Powinien być szybszy w przypadku tworzenia, prawda? Poszedłem do przodu i przedstawiłem odpowiedź, demonstrując, co mam na myśli. – mlissner

-3

Lub jeśli po prostu chcesz licznik, a nie trwałe obiektu można korzystać itertools licznik, który jest zaimplementowany w C. Gil zapewni potrzebne bezpieczeństwo.

--Sai

+0

Pytający specjalnie pytał, w jaki sposób atomowo zwiększyć pole w bazie danych. – slacy

+0

Komentarz Ditto slacy –

+0

W obronie tego gościa, nie jest to wyraźnie powiedziane. Oczywiście jednak zamierzone. –

14

W Django 1.4 jest support for SELECT ... FOR UPDATE klauzule, używając blokady bazy danych, aby upewnić się, że żadne dane nie są jednocześnie dostęp przez pomyłkę.

+0

To było rozwiązanie, które zakończyłem w połączeniu z zawijaniem bloku w transaction.commit_on_success. – Bialecki

4

ich prostota i budynek na użytkownika @ Oduvan odpowiedź:

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

Zaletą jest to, że jeśli obiekt został stworzony w pierwszym piśmie, nie trzeba wykonywać żadnych dalszych aktualizacji.

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

Jeśli nie trzeba znać wartość licznika gdy go ustawić górny odpowiedź jest zdecydowanie najlepszym:

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

mówi to baza danych, aby dodać 1 do wartości count, która może doskonale działać bez blokowania innych operacji. Wadą jest to, że nie masz możliwości dowiedzenia się, co właśnie ustawiłeś. Jeśli dwa wątki jednocześnie uderzyłyby w tę funkcję, obaj zobaczyliby tę samą wartość i obaj powiedzieliby dbowi o dodaniu 1. Baza danych zakończy się dodaniem 2 zgodnie z oczekiwaniami, ale nie będzie wiadomo, która z nich poszła pierwsza.

Jeśli teraz dbasz o liczbę, możesz skorzystać z opcji select_for_update, do której odwołuje się Emil Stenstrom. Oto, jak to wygląda:

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

To odczytuje bieżącą wartość i blokuje pasujące wiersze do końca transakcji. Teraz tylko jeden pracownik może czytać na raz. Więcej informacji na temat select_for_update można uzyskać pod numerem the docs.

+0

Ta odpowiedź ma najlepsze wytłumaczenie. Dopiero po przeczytaniu tego jednego byłem przekonany, że 'count = F ('count') + 1' zadziała –