-
Есть view примерно такого вида:
Если работает в один поток, то все ок.item = Post.objects.get(...)
item.views += 1
item.save()
Но если будет 2 или более, то учитываются не всё.
Чем можно заменить такую конструкцию? -
Если под потоками вы имеете в виду threads, то лучше всего там не использовать Django models вообще. Открывать отдельный connection к базе для каждого потока и работать с SQL напрямую. Одна из проблем, насколько я помню, что Django пытается использовать одно соединение для всех потоков и у базы срывает крышу.
-
а есть где нить в настройках, чтоб Django не использовало одно соединение для всех потоков?
-
Я когда-то читал в блоге (найду, сброшу ссылку), что использование одного соединения для всех потоков это не единственная проблема с моделями. Человек пропатчил джангу для использования отдельного коннекта на thread для моделей.
Не помогло. Рекомендация была однозначной. См. совет выше. -
один коннект на несколько потоков это как-то сомнительно. насколько я понимаю общий подход такой: имеется пул потоков\процессов и у каждого свой коннект к базе, и по мере прихода клиентов из этого пула назначается поток\процесс который будет отвечать за обработку клиента.
по поводу счётчика:
0) подумать так ли уж нужно точное значение просмотров;
1) можно делать прямой sql: update app_post set views=views+1 where id=123;
2) можно обернуть в python-коде этот кусок lock'ом -
0) я делал тест ab -n10000 -c 10 в итоге вместо 10000 просмотров, защиталось 1600 гдето.
1) это вариант наиболее подходит -
Если под потоками вы имеете в виду threads, то лучше всего там не использовать Django models вообще. Открывать отдельный connection к базе для каждого потока и работать с SQL напрямую. Одна из проблем, насколько я помню, что Django пытается использовать одно соединение для всех потоков и у базы срывает крышу.
Не надо вводить народ в заблуждение! Джанго в тредах работает нормально. В частности, тикет с собственными коннектами на каждый тред был сделан еще два с половиной года назад.
Проблема с счетчиком на самом деле в другом. Если два параллельных процесса (или треда, что абсолютно неважно), делают изменение в базе, они его делают каждый в своей транзакции. И то, насколько эти транзакции видят то, что происходит вокруг, зависит от их isolation level'а. Тот, который используется везде по умолчанию (READ COMMITED) говорит о том, что транзакция не видит результатов других незакомиченных транзакций. Поэтому даже если мы делаем запрос типа:
update table set views = views + 1;То если он происходит в параллельных транзакциях, они увеличат счетчик только на 1, ни одна из них не будет ждать, пока вторая закончится. В принципе, исправляется это установкой isolation level SERIALIZABLE (кажется), но цена корректности в этом случае в том, что все будет лочится и друг друга ждать.
В общем, как решать задачу корректно, я тоже не знаю. Наверное бы вообще для этого в базу не смотрел. Набирал бы например, лог на файловой системе, дописывая в залочиваемый файл id страницы. А раз в N минут разбирал бы его отдельным процессом, который бы читал все idшки и прибавлял к полям в базе.
Но вообще мне кажется, что народ в основном не парится с корректностью. Если делать
update ... set, как я выше показал, расхождений будет сильно поменьше, чем в простом случае "считать число", "почесаться в памяти", "увеличить на единицу". Good enough :-) -
Кстати, там Михаил советует:
можно обернуть в python-коде этот кусок lock'ом
Это поможет только в многотредной среде, в многопроцессной локи разных процессов друг друга не видят.
Кстати, можно как раз на файловой системе лок устроить. Типа:
from django.core.files import locks try: f = open('./file', 'wb') locks.lock(f, locks.LOCK_EX) from django.db import connection cursor = connection.cursor() cursor.execute('update page set views = views + 1 wher id = %s', [id]) cursor.close() finally: f.close()Тогда да, каждый инкремент будет происходить атомарно.
-
Так это, можно наверно и таблицу лочить.
-
Ну да, наверное...
-
Сейчас Иван опять будет ругаться, что я не по делу memcached использую :)
А не проще ли писать в memcached, а оттуда переодически загонять в базу? Да, там будут маленькие погрешности, т.к. memcached можно лочить только искусственно и не "наверняка", но скорость работы будет хорошей.
Если конечно, случайная потеря куска данных между переодическими загонами в БД вас не испугает (ИМХО - это не столь страшно и весьма маловероятно). -
Конечно буду ругаться :-). Завязка на конкретный кешовый бэкенд, вообще говоря, сильно уменьшает переносимость приложения. Не все используют memcached.
Кроме того, аргумент про скорость тут не срабатывает, потому что лочить один файл на короткое время — это очень быстро. Можно даже не начинать думать, что это будет тормозить.
P.S. Хотя по правде говоря надо просто оторвать всю фичу :-). Все эти "количества просмотров" никогда особо не нужны никому.
-
Сохранять кол-во просмотров напрямую в базу мне кажется не совсем разумной идеей, особенно лок на всю таблицу. Производительность упадет прилично.
Лучше сделать как написал Сергей Тарасенко — аккумулировать данные в memcached и периодически сбрасывать их в базу. -
Сохранять кол-во просмотров напрямую в базу мне кажется не совсем разумной идеей, особенно лок на всю таблицу. Производительность упадет прилично.
Ребят, перестаньте оценивать производительность "на глаз" :-). Откуда вообще идея, что лок на таблицу или лок на файл как-то медленнее, чем лок в memcached?
-
> В общем, как решать задачу корректно, я тоже не знаю.
При просмотре view делать update view_count set cnt=nextval('view_count_seq').
обнуление - установка значения счетчика и содержимого ячейки таблицы.
Преимущества - никаих локов (явных, есть неявные на update строки).
Косяки - (нестандатно для Django), а именно:
- секвенс придется делать руками (т.е. всякие миграции, переносы через syncdb не пройдут)
- работает только с postgres (в MySQL это предлагают делать хаком, который работает не всегда, секвенсов полноценных там нет).
http://www.postgresql.org/docs/8.3/static/sql-createsequence.html
Локи на файлы - ужаснейшее решение. -
>> Откуда вообще идея, что лок на таблицу или лок на файл как-то медленнее, чем лок в memcached?
Я этого не говорил. В memcached я предлагал писать вообще без лока, т.к. точность до каждого запроса тут не критична, для точности есть логи.
На счет файлов ничего не скажу — не пробовал.
Что касается лока на всю таблицу — постоянные апдейты с каждым запросом и лок при определенной нагрузке приводят к тормозам, мне казалось это очевидным, разве нет? -
Использовать логирование в файлах - хорошо. Но зачем давать дополнительную нагрузку на винты, когда это можно держать в памяти?
Хотя в случае простого, среднего проекта это маловажно, а в случае сложного нужны уже иные пути.
Потому, видимо файлы действительно лучше. -
а чем плохо если подсчёт делать отдельным запросом от клиента, например просто вставив на нужные страницы <img src="counter.gif?app.post.123"/>, и уже этот запрос ловить\считать?
из плюсов:
- можно так подсчитывать 'любой' контент-объект,
- даже если на счётчике и будут тормоза, то на выдачу основного контента это не повлияет и этих тормозов клиент и не заметит. -
Михаил, речь идет не о клиенте, а о сервере.
-
При просмотре view делать update viewcount set cnt=nextval('viewcount_seq').
Не, сиквенсы не пройдут. Потому что нужен отдельный счетчик на каждую запись таблицы.
-
Использовать логирование в файлах - хорошо. Но зачем давать дополнительную нагрузку на винты, когда это можно держать в памяти?
Этот файл будет висеть физически в памяти (в кеше ОС) практически все время. Файлы — это очень-очень быстро, правда :-)
-
если клиент не чувстует тормозов, то есть они или нет на сервере уже не так принципиально.
я к тому что если отделить счётчик от основного контента в отдельную модель, то будет там lock через файл, на уровне базы или ещё, на выдачу контента это не повлияет(я так думаю):class Counter(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.IntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
count = models.IntegerField(u"Счётчик", default=0) -
> Не, сиквенсы не пройдут. Потому что нужен отдельный счетчик на каждую запись таблицы.
да, действительно, на каждую запись в таблице post.
можно сделать очень хак. завести кроме views еще поле lock и ПЕРЕД считыванием views делать update lock. Затем select views from post и в конце update post set views.
Хитрость в том, что пока одна транзакция, ОБНОВИВШАЯ поле lock не завершиться - другой не дадут его обновить (только читать), а пока она не обновит его, она не считает текущие views.
Опять же только для postgres,
работать с полями придется голым SQL, чтобы Django ничего не закешировал. -
Кстати, еще одно решение нарисовалось. Лучше, чем лок на таблицы: делать для поля счетчика
select for update:select views from post where id = ... for update;Тогда это будет лок только на одну запись, и никому не будет мешать.
-
Вань, вот только MyISAM, вроде все равно не умеет лочить построчно, так что будет только для InnoDB работать. Ну и опять же, от СУБД сильно зависимо получается.
-
Да по сути только MyISAM и отбрасывается. И честно говоря, это хорошо. Еще один гвоздь в гроб :-)
-
Я сделал так: update app_post set views=views+1 where id=123;
Попробовал тест ab -n10000 -c10
Посчитало точно, без разхождений -
кстати если уж от многопоточности перешли к многопроцессовости, то расширении до многохостовости(так кажется) видимо наиболее простым в реализации будет именно блокировка на уровне базы.
-
@vluki, а какая база при этом была? Тот же MyISAM автоматически лочит всю таблицу при записи/обновлении так что расхождений по любому не будет.
-
MySQL/InnoDB





