Подтвердить что ты не робот

Атомное приращение счетчика в джанго

Я пытаюсь атомарно увеличивать простой счетчик в 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()

Если я правильно понимаю Django, это должно обернуть функцию в транзакции и сделать прирост атомом. Но это не работает, и в обновлении счетчика есть условие гонки. Как этот код можно сделать потокобезопасным?

4b9b3361

Ответ 1

Новое в Django 1.1

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

или используя выражение F:

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

Ответ 2

В Django 1.4 существует поддержка предложений SELECT... FOR UPDATE, используя блокировки базы данных, чтобы гарантировать, что никакие данные не получаются одновременно по ошибке,

Ответ 3

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

Сохраняя это просто и опираясь на ответ @Oduvan:

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

Преимущество в том, что если объект был создан в первом выражении, вам не нужно делать никаких дополнительных обновлений.

Ответ 5

Если вам не нужно знать значение счетчика, когда вы его устанавливаете, лучшим ответом, безусловно, будет лучший выбор:

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

Это сообщает вашей базе данных добавить 1 к значению count, который он может сделать отлично, не блокируя другие операции. Недостатком является то, что вы не знаете, что именно count вы только что установили. Если два потока одновременно попадают в эту функцию, они оба будут видеть одно и то же значение, и оба будут сообщать db о добавлении 1. ДБ в итоге добавит 2, как ожидалось, но вы не узнаете, какой из них был первым.

Если вы сейчас интересуетесь счетом, вы можете использовать опцию select_for_update, на которую ссылается Эмиль Стэнстром. Вот что это выглядит:

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

Это считывает текущее значение и блокирует совпадающие строки до конца транзакции. Теперь только один рабочий может читать за раз. См. документы для получения дополнительной информации о select_for_update.

Ответ 6

Или, если вам просто нужен счетчик, а не постоянный объект, вы можете использовать счетчик itertools, который реализован в C. GIL обеспечит необходимую безопасность.

- Sai