1. vluki

    20.11.2008 00:37

    Есть view примерно такого вида:
    item = Post.objects.get(...)
    item.views += 1
    item.save()
    Если работает в один поток, то все ок.
    Но если будет 2 или более, то учитываются не всё.

    Чем можно заменить такую конструкцию?
  2. Dyadya Zed

    20.11.2008 02:22

    Если под потоками вы имеете в виду threads, то лучше всего там не использовать Django models вообще. Открывать отдельный connection к базе для каждого потока и работать с SQL напрямую. Одна из проблем, насколько я помню, что Django пытается использовать одно соединение для всех потоков и у базы срывает крышу.
  3. tony

    20.11.2008 09:03

    а есть где нить в настройках, чтоб Django не использовало одно соединение для всех потоков?
  4. Dyadya Zed

    20.11.2008 11:47

    Я когда-то читал в блоге (найду, сброшу ссылку), что использование одного соединения для всех потоков это не единственная проблема с моделями. Человек пропатчил джангу для использования отдельного коннекта на thread для моделей.
    Не помогло. Рекомендация была однозначной. См. совет выше.
  5. Михаил

    20.11.2008 12:01

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

    по поводу счётчика:
    0) подумать так ли уж нужно точное значение просмотров;
    1) можно делать прямой sql: update app_post set views=views+1 where id=123;
    2) можно обернуть в python-коде этот кусок lock'ом
  6. vluki

    20.11.2008 13:37

    0) я делал тест ab -n10000 -c 10 в итоге вместо 10000 просмотров, защиталось 1600 гдето.
    1) это вариант наиболее подходит
  7. Иван Сагалаев

    20.11.2008 14:48

    Если под потоками вы имеете в виду 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 :-)

  8. Иван Сагалаев

    20.11.2008 14:53

    Кстати, там Михаил советует:

    можно обернуть в 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()
    

    Тогда да, каждый инкремент будет происходить атомарно.

  9. Big 40wt Svetlyak

    20.11.2008 16:09

    Так это, можно наверно и таблицу лочить.
  10. Иван Сагалаев

    20.11.2008 16:25

    Ну да, наверное...

  11. Сейчас Иван опять будет ругаться, что я не по делу memcached использую :)

    А не проще ли писать в memcached, а оттуда переодически загонять в базу? Да, там будут маленькие погрешности, т.к. memcached можно лочить только искусственно и не "наверняка", но скорость работы будет хорошей.

    Если конечно, случайная потеря куска данных между переодическими загонами в БД вас не испугает (ИМХО - это не столь страшно и весьма маловероятно).
  12. Иван Сагалаев

    20.11.2008 18:09

    Конечно буду ругаться :-). Завязка на конкретный кешовый бэкенд, вообще говоря, сильно уменьшает переносимость приложения. Не все используют memcached.

    Кроме того, аргумент про скорость тут не срабатывает, потому что лочить один файл на короткое время — это очень быстро. Можно даже не начинать думать, что это будет тормозить.

    P.S. Хотя по правде говоря надо просто оторвать всю фичу :-). Все эти "количества просмотров" никогда особо не нужны никому.

  13. priestley

    20.11.2008 18:09

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

    Лучше сделать как написал Сергей Тарасенко — аккумулировать данные в memcached и периодически сбрасывать их в базу.
  14. Иван Сагалаев

    20.11.2008 18:13

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

    Ребят, перестаньте оценивать производительность "на глаз" :-). Откуда вообще идея, что лок на таблицу или лок на файл как-то медленнее, чем лок в memcached?

  15. flexair.ya.ru

    20.11.2008 18:21

    > В общем, как решать задачу корректно, я тоже не знаю.

    При просмотре 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

    Локи на файлы - ужаснейшее решение.
  16. priestley

    20.11.2008 18:24

    >> Откуда вообще идея, что лок на таблицу или лок на файл как-то медленнее, чем лок в memcached?

    Я этого не говорил. В memcached я предлагал писать вообще без лока, т.к. точность до каждого запроса тут не критична, для точности есть логи.

    На счет файлов ничего не скажу — не пробовал.

    Что касается лока на всю таблицу — постоянные апдейты с каждым запросом и лок при определенной нагрузке приводят к тормозам, мне казалось это очевидным, разве нет?
  17. Использовать логирование в файлах - хорошо. Но зачем давать дополнительную нагрузку на винты, когда это можно держать в памяти?
    Хотя в случае простого, среднего проекта это маловажно, а в случае сложного нужны уже иные пути.

    Потому, видимо файлы действительно лучше.
  18. Михаил

    20.11.2008 18:36

    а чем плохо если подсчёт делать отдельным запросом от клиента, например просто вставив на нужные страницы <img src="counter.gif?app.post.123"/>, и уже этот запрос ловить\считать?

    из плюсов:
    - можно так подсчитывать 'любой' контент-объект,
    - даже если на счётчике и будут тормоза, то на выдачу основного контента это не повлияет и этих тормозов клиент и не заметит.
  19. Михаил, речь идет не о клиенте, а о сервере.
  20. Иван Сагалаев

    20.11.2008 18:45

    При просмотре view делать update viewcount set cnt=nextval('viewcount_seq').

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

  21. Иван Сагалаев

    20.11.2008 18:46

    Использовать логирование в файлах - хорошо. Но зачем давать дополнительную нагрузку на винты, когда это можно держать в памяти?

    Этот файл будет висеть физически в памяти (в кеше ОС) практически все время. Файлы — это очень-очень быстро, правда :-)

  22. Михаил

    20.11.2008 18:53

    если клиент не чувстует тормозов, то есть они или нет на сервере уже не так принципиально.
    я к тому что если отделить счётчик от основного контента в отдельную модель, то будет там 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)
  23. flexair.ya.ru

    20.11.2008 19:03

    > Не, сиквенсы не пройдут. Потому что нужен отдельный счетчик на каждую запись таблицы.

    да, действительно, на каждую запись в таблице post.

    можно сделать очень хак. завести кроме views еще поле lock и ПЕРЕД считыванием views делать update lock. Затем select views from post и в конце update post set views.

    Хитрость в том, что пока одна транзакция, ОБНОВИВШАЯ поле lock не завершиться - другой не дадут его обновить (только читать), а пока она не обновит его, она не считает текущие views.

    Опять же только для postgres,
    работать с полями придется голым SQL, чтобы Django ничего не закешировал.
  24. Иван Сагалаев

    20.11.2008 19:07

    Кстати, еще одно решение нарисовалось. Лучше, чем лок на таблицы: делать для поля счетчика select for update:

    select views from post where id = ... for update;
    

    Тогда это будет лок только на одну запись, и никому не будет мешать.

  25. Big 40wt Svetlyak

    20.11.2008 19:55

    Вань, вот только MyISAM, вроде все равно не умеет лочить построчно, так что будет только для InnoDB работать. Ну и опять же, от СУБД сильно зависимо получается.
  26. Иван Сагалаев

    20.11.2008 21:54

    Да по сути только MyISAM и отбрасывается. И честно говоря, это хорошо. Еще один гвоздь в гроб :-)

  27. vluki

    21.11.2008 10:44

    Я сделал так: update app_post set views=views+1 where id=123;
    Попробовал тест ab -n10000 -c10
    Посчитало точно, без разхождений
  28. Михаил

    21.11.2008 10:56

    кстати если уж от многопоточности перешли к многопроцессовости, то расширении до многохостовости(так кажется) видимо наиболее простым в реализации будет именно блокировка на уровне базы.
  29. Big 40wt Svetlyak

    21.11.2008 12:15

    @vluki, а какая база при этом была? Тот же MyISAM автоматически лочит всю таблицу при записи/обновлении так что расхождений по любому не будет.

  30. vluki

    21.11.2008 13:40

    MySQL/InnoDB

bbcode