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

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


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


Ответы (6)


Используйте выражение 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-выражений добавлено в официальную документацию.

person Oduvan    schedule 21.10.2009
comment
следует обернуть это в метод commit_on_success? - person alexef; 30.11.2011
comment
Одна из проблем заключается в том, что если вам впоследствии понадобится обновленное значение, вам нужно будет получить его из базы данных. В некоторых случаях, например при генерации идентификатора, это может вызвать состояние гонки. Например, два потока могут атомарно увеличивать идентификатор (скажем, с 1 до 3), но затем оба запрашивают текущее значение и получают 3, пытаются вставить, взрыв ... Просто о чем подумать. - person Bialecki; 03.04.2012
comment
Во второй версии, почему бы не использовать kwarg по умолчанию для get_or_create, а затем поместить объект F в блок if created? Должно быть быстрее в случае создания, не так ли? Я пошел дальше и поставил ответ, демонстрирующий, что я имею в виду. - person mlissner; 22.09.2013
comment
Это определенно правильный ответ. Ознакомьтесь с django doc о F (): Еще одно преимущество использования F () заключается в том, что обновление значения поля в базе данных, а не в Python, позволяет избежать состояния гонки. - person Han He; 12.02.2014
comment
get_or_create возвращает пару, поэтому она должна быть counter, created = ..., как в ответе Млисснера. - person Alex Hall; 11.12.2016
comment
@Bialecki, вы можете просто написать new_count = counter.count + 1 перед обновлением. - person Alex Hall; 11.12.2016
comment
Итак, в основном после обновления такого поля модель будет содержать экземпляр django.db.models.expressions.CombinedExpression вместо фактического результата. Если вы хотите сразу получить доступ к результату: counter.refresh_from_db () - person Nandesh; 25.10.2018
comment
Время от времени мне это не удается, и я получаю меньшее количество, чем ожидалось. Поддерживает ли это каждый сервер базы данных? В частности, поддерживает ли это sqlite3? - person Whadupapp; 07.02.2019

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

person Emil Stenström    schedule 17.01.2012
comment
Это было решение, к которому я пришел, в сочетании с переносом блока в transaction.commit_on_success. - person Bialecki; 03.04.2012
comment
Точно. Настоящая проблема - это гонка одних и тех же данных разными клиентами. Внутренняя блокировка может гарантировать, что каждый клиент получит свой собственный серийный номер для своей транзакции. - person George Y; 15.04.2021

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

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.

person Xephryous    schedule 29.09.2017
comment
У этого ответа есть лучшее объяснение. Только когда я прочитал это, я убедился, что count = F('count') + 1 сработает. - person Aaron McMillin; 20.11.2017

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

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

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

person mlissner    schedule 21.09.2013

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()
person derevo    schedule 30.01.2015

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

- Сай

person Sai Venkat    schedule 26.08.2010
comment
Спрашивающий специально спрашивал, как атомарно увеличивать поле в базе данных. - person slacy; 27.01.2012
comment
В защиту этого парня об этом прямо не говорится. Хотя явно задумано. - person N. McA.; 24.09.2016