2008-11-11 5 views
24

Я пытаюсь выполнить (что я думаю) довольно простая модель данных для счетчика:Атомные операции в Джанго?

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 

Когда кто-то приходит через, он будет искать строку, которая соответствует visitType и visitDate; если эта строка не существует, она будет создана с помощью счетчика = 0.

Затем мы увеличиваем счетчик и сохраняем.

Я обеспокоен тем, что этот процесс является полностью гоночным. Два запроса могли одновременно проверять, существует ли объект, и оба из них могут его создать. Между чтением счетчика и сохранением результата может пройти другой запрос и увеличить его (что приведет к потере счета).

До сих пор я не нашел хороший способ обойти это ни в документации Django, ни в учебнике (на самом деле, похоже, что у учебника есть условие гонки в части Vote).

Как это сделать безопасно?

ответ

1

Это немного взломанный. Необработанный SQL сделает ваш код менее портативным, но он избавится от состояния гонки при приращении счетчика. Теоретически это должно увеличивать счетчик при каждом запросе. Я не тестировал это, поэтому вы должны убедиться, что список правильно интерполируется в запросе.

class VisitorDayTypeCounterManager(models.Manager): 
    def get_query_set(self): 
     qs = super(VisitorDayTypeCounterManager, self).get_query_set() 

     from django.db import connection 
     cursor = connection.cursor() 

     pk_list = qs.values_list('id', flat=True) 
     cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list]) 

     return qs 

class VisitorDayTypeCounter(models.Model): 
    ... 

    objects = VisitorDayTypeCounterManager() 
5

два предложения:

Добавить unique_together в модель, и обертывают создание в обработчик исключений поймать дубликаты:

class VisitorDayTypeCounter(models.Model): 
    visitType = models.CharField(max_length=60) 
    visitDate = models.DateField('Visit Date') 
    counter = models.IntegerField() 
    class Meta: 
     unique_together = (('visitType', 'visitDate')) 

После этого, вы можете stlll иметь незначительные условия гонки на счетчик. Если у вас будет достаточно трафика, чтобы быть обеспокоенным этим, я бы предложил изучить транзакции для более тонкого управления базами данных. Я не думаю, что ORM имеет прямую поддержку блокировки/синхронизации. Документация по транзакциям доступна here.

+0

unique_together, конечно, заставляет меня чувствовать себя немного более комфортно. Вероятно, на этом не будет достаточно трафика, чтобы заставить гонку попасть, но поскольку я изучаю Django в то же время, я решил, что хочу «сделать это правильно». Спасибо за помощь! – 2008-11-11 06:07:58

+0

Да, я тебя слышу. Может быть, кто-то еще здесь будет знать об ORM-функции для обработки этого или может прояснить, являются ли некоторые из встроенных модулей безопасными для этого сценария. – 2008-11-11 06:35:07

1

Почему бы не использовать базу данных в качестве уровня параллелизма? Добавьте первичный ключ или уникальное ограничение таблицы для посещенияType и visitDate. Если я не ошибаюсь, django точно не поддерживает это в своей базе данных Model или, по крайней мере, я не видел примера.

После того, как вы добавили ограничение/ключ к таблице, то все, что вам нужно сделать, это:

  1. проверить, если строка есть. если это так, выберите его.
  2. Вставить строку. если нет ошибок, вы в порядке и можете двигаться дальше.
  3. если есть ошибка (то есть состояние гонки), повторите выбор строки. если нет строки, то это настоящая ошибка. В противном случае, все в порядке.

Это противно делать это таким образом, но это кажется достаточно быстрым и охватит большинство ситуаций.

+0

Он не обрабатывает случай, когда два человека идут обновлять счетчик одновременно. – 2008-11-11 06:28:07

0

Чтобы избежать такого рода состояний гонки, вы должны использовать транзакции базы данных. Транзакция позволяет выполнять всю операцию создания, чтения, увеличения и сохранения счетчика на базе «все или ничего». Если что-то пойдет не так, оно отбросит все это, и вы сможете попробовать еще раз.

Проверьте Django docs. Существует промежуточная служба транзакций, или вы можете использовать декораторы вокруг представлений или методов для создания транзакций.

+0

Я согласен с тем, что транзакции выглядят как ответ здесь, но неясно, что функциональность действительно решит проблему приращения - SELECT, чтобы получить строку, все равно будет успешной, и UPDATE, чтобы изменить значение счетчика, все равно будет успешным. Если я ошибаюсь, пример будет потрясающим. – 2008-11-11 13:39:57

+0

Вам нужно будет заблокировать стол во время выбора, чтобы сделать это таким образом, и, как упоминалось Сэмом, это приведет к снижению производительности. Это лучший способ, если вы часто не набираете счетчик. – 2008-11-12 03:20:56

12

Если вы действительно хотите, чтобы счетчик был точным, вы могли бы использовать транзакцию, но требуемая сумма параллелизма действительно перетащит ваше приложение и базу данных под любую значительную нагрузку.Вместо этого подумайте о том, чтобы перейти к более удобному подходу к настройкам обмена сообщениями и просто хранить записи счетчиков в таблицу для каждого посещения, где вы хотите увеличить счетчик. Затем, когда вы хотите, чтобы общее количество посещений делало счет в таблице посещений. У вас также может быть фоновый процесс, который выполняется любое количество раз в день, которое суммирует посещения, а затем сохраняет их в родительской таблице. Чтобы сэкономить место, он также удалит любые записи из таблицы посещений, которые он суммировал. Вы сократите свои расходы на параллелизм огромной суммой, если у вас нет нескольких агентов, соперничающих за одни и те же ресурсы (счетчик).

+0

Эй, хороший звонок! В основном я занимаюсь работой в App Engine, и я повесил трубку на «транзакции, действующие только на одну запись», и «выполнение агрегатных функций очень дорого». Это очень простой способ решить проблему. Благодаря! – 2008-11-11 18:27:24

+0

Я полагаю, что это зависит от того, будет ли процесс тяжелым или тяжелым. Подсчет будет читаться намного чаще, чем в моей системе, поэтому для проблемы, как указано, это может быть не лучший план. Тем не менее, он решает другие проблемы, которые у меня были, поэтому спасибо! – 2008-11-11 19:28:21

6

Вы можете использовать патч от http://code.djangoproject.com/ticket/2705 для блокировки уровня базы данных базы данных.

С патчем этот код будет атомное:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update() 
visitors.counter += 1 
visitors.save() 
26

На Django 1.1 вы можете использовать F() выражения ОРМ в.

from django.db.models import F 
product = Product.objects.get(name='Venezuelan Beaver Cheese') 
product.number_sold = F('number_sold') + 1 
product.save() 

Для получения более подробной информации обратитесь к документации:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

 Смежные вопросы

  • Нет связанных вопросов^_^