Bezdepbonusu7Программирование: статьи и ответы

Вопрос

Атомарное приращение счетчика в django

Автор вопроса: Björn LindqvistОтветов: 6

Я пытаюсь атомарно увеличить простой счетчик в 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, это должно заключить функцию в транзакцию и сделать приращение атомарным. Но это не работает, и в обновлении счетчика есть состояние гонки. Как сделать этот код поточно-ориентированным?

  • На мой взгляд, не использовать += во избежание состояния гонки - такая трата времени. Пользователи Python уже должны знать, что между a += b и a = a + b есть разница, так почему бы не использовать это? Может это будет противоречить каким-то данным кеша? Точно сказать не могу. aliqandil 19.04.2017

pythondjangorace-conditiontransactions

Ответы

Ответ 1 · рейтинг 103 · Oduvan

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

from django.db.models import F

либо в update():

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

или на экземпляре объекта:

counter, _ = Counter.objects.get_or_create(name=name)
counter.count = F("count") + 1
counter.save(update_fields=["count"])

Не забудьте указать update_fields, иначе вы можете столкнуться с условиями гонки в других полях модели.

Примечание об состоянии гонки, которого удалось избежать с использованием F-выражений добавлено в официальную документацию.

Ответ 2 · рейтинг 18 · Emil Stenström

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

Ответ 3 · рейтинг 16 · Xephryous

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

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

Это говорит вашей базе данных добавить 1 к значению count, что она может отлично справиться, не блокируя другие операции. Недостатком является то, что у вас нет возможности узнать, что count вы только что установили. Если два потока одновременно задействуют эту функцию, они оба увидят одно и то же значение и оба скажут базе данных добавить 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.

Ответ 4 · рейтинг 8 · mlissner

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

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

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

Ответ 5 · рейтинг 5 · derevo

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

Ответ 6 · рейтинг -2 · Sai Venkat

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

- Сай

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы