Форумы PHP - как справиться с непрочитанными обсуждениями /темами/сообщениями


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

О непрочитанных обсуждениях/темах/сообщениях есть о чем подумать. Я не знаю, как работают форумные системы, такие как MyBB, Vбуллет, Плата питания Invision, Ваниль, phpBB и т.д., справьтесь с этим проблема, поэтому я хотел бы почитать у вас, ребята, ваш опыт в этом вопросе. Я знаю, что использование таблицы базы данных только для этого - самый простой способ, но это потребует огромного чтения/записи, когда сообщество насчитывает более 10 000 участников и 1000 новых тем каждый месяц. Это сложно, но должен быть способ избежать перегрузки сервера.

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

Author: Peter Mortensen, 2010-02-18

7 answers

Вариантов не так уж много.

  1. Отметьте каждый поток чтения каждым пользователем.

    • Недостатки: много строк на очень активных форумах
    • Преимущества: Каждый пользователь знает, прочитал пост или нет.
  2. Отметьте каждую непрочитанную тему каждым пользователем.

    • Недостатки: много места с "непрочитанными" строками, если есть неактивность большого количества пользователей
    • Решения: добавьте метку времени жизни и удалите старые записи с помощью крон
    • Преимущества: Каждый пользователь знает, прочитал пост или нет.
  3. Используйте метки времени, чтобы определить, показывать ли его как непрочитанное или нет.

    • Недостатки: Пользователи не знают, с какими реальными непрочитанными потоками, метки показывают только "новые заголовки" с момента последнего входа в систему
    • Преимущество: Экономия места

Другой альтернативой является смешивание растворов, то есть

1 и 3) показывать поток как "непрочитанный", если они не старше X дней и для пользователя нет строки, помеченной как прочитанная. Строки "чтение" могут быть удалены, когда они на X дней старше, без каких-либо последствий.

Преимущества

  • меньший интервал используется для определения непрочитанных потоков

Недостатки

  • создайте cron, который поддерживает чистоту системы
  • Пользователи не знают, читают ли они темы старше x дней.

Преимущества

  • Каждый пользователь знает, какие "новые сообщения" прочитал или нет.
 15
Author: useless, 2014-02-24 09:06:50

Есть... еще один.

Еще один способ хранения подробных прочитанных/непрочитанных данных для иерархической структуры форума (доска > раздел > тема и т.д.). Он делает это без а) необходимости предварительного заполнения прочитанной/непрочитанной информации и б) без необходимости хранить более U*(M/2) строк в худшем случае, где U - количество пользователей, а M - общее количество записей в базе данных (и обычно намного, намного меньше этого)

Я исследовал эту тему некоторое время назад. Я обнаружил, что SMF/phpBB немного "обманывают" в том, как они хранят историю чтения пользователей. Их схема поддерживает хранение либо последних меток времени, либо идентификатора сообщения, которое было помечено как прочитанное на данной доске, форуме, подфоруме, теме (или просмотрено непосредственно браузером), например:

[идентификатор пользователя, доска, last_msg_id, отметка последнего времени]

[идентификатор пользователя, доска, форум, last_msg_id, отметка последнего времени]

[идентификатор пользователя, доска, форум, подфорум, идентификатор last_msg_id, отметка последнего времени]

[идентификатор пользователя, доска, форум, подфорум, тема, last_msg_id, отметка последнего времени]

Это позволяет пользователям отмечать определенные доски, форумы, темы и т.д. как "прочитанные". Однако для этого требуется либо действие со стороны пользователя (либо чтение, либо активное нажатие кнопки "отметить как прочитанное"), и в случае phpBB это не дает вам возможности сказать "Я видел это конкретное сообщение, но не это конкретное сообщение". Вы также получаете ситуацию, когда сначала читаете последнее сообщение в теме (просматриваете последние действия в поток), и вы сразу же предполагаете, что прочитали остальную часть потока.

Это работает для SMF и phpBB для хранения подобных вещей, потому что вы редко просматриваете только одно сообщение (просмотры по умолчанию настроены для 20+ сообщений на последней странице темы). Однако для более многопоточных форумов (особенно форумов, где вы просматриваете сообщения по одному за раз) это далеко не идеально. Пользователям этой системы, скорее всего, будет очень важно, прочитали ли они одно сообщение, но не другое, и может показаться неудобным отмечать весь раздел как прочитанный, когда на самом деле они просто хотели, чтобы некоторые из них были помечены как прочитанные.

Вы храните сообщения в кортежах, подобных этому: [идентификатор пользователя, нижний идентификатор_сг_ид, верхний идентификатор_сг_ид]

Журнал истории пользователей ведется следующим образом:

При просмотре страницы функция проверяет, есть ли у user_id запись, в которой current_msg_id находится между нижним_мсг_идом и верхним_мсг_идом. Если да, то эта страница прочитана, и никаких действий предпринимать не нужно. Если это если нет, то необходимо выполнить еще один запрос, на этот раз определяющий, является ли current_msg_id либо на единицу меньше, чем lower_msg_id (current_msg_id==lower_msg_id-1), либо на единицу больше, чем upper_msg_id (current_msg_id==upper_msg_id +1). Это тот случай, когда мы увеличиваем нашу границу "прочитанного" или "увиденного" на 1. Если мы находимся на расстоянии одного от lower_msg_id или upper_msg_id, то мы увеличиваем кортеж на 1 в этом направлении. Если мы не увеличиваем диапазон кортежей, то вставляем новый кортеж [идентификатор пользователя, current_msg_id, current_msg_id].

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

Пример кода на PHP:

function seen_bounds( $usr_id, $msg_id ) {

    # mysql escape
    $usr_id = mres( $usr_id );
    $msg_id = mres( $msg_id );

    $seen_query = "
        SELECT
            msb.id,
            msb.lower_msg_id,
            msb.upper_msg_id
        FROM
            msgs_seen_bounds msb
        WHERE
            $msg_id BETWEEN msb.lower_msg_id AND msb.upper_msg_id AND
            msb.usr_id = $usr_id
        LIMIT 1;
    ";

    # See if this post already exists within a given
    # seen bound.
    $seen_row = query($seen_query, ROW);

    if($seen_row == 0) {
        # Has not been seen, try to detect if we're "near"
        # another bound (and we can grow that bound to include
        # this post).
        $lower_query = "
            SELECT
                msb.id,
                msb.lower_msg_id,
                msb.upper_msg_id
            FROM
                msgs_seen_bounds msb
            WHERE
                msb.upper_msg_id = ($msg_id - 1) AND
                msb.usr_id = $usr_id
            LIMIT 1;
        ";

        $upper_query = "
            SELECT
                msb.id,
                msb.lower_msg_id,
                msb.upper_msg_id
            FROM
                msgs_seen_bounds msb
            WHERE
                msb.lower_msg_id = ($msg_id + 1) AND
                msb.usr_id = $usr_id
            LIMIT 1;
        ";

        $lower = query($lower_query, ROW);
        $upper = query($upper_query, ROW);

        if( $lower == 0 && $upper == 0 ) {
            # No bounds exist for or near this. We'll insert a single-ID
            # bound

            $saw_query = "
                INSERT INTO
                    msgs_seen_bounds
                (usr_id, lower_msg_id, upper_msg_id)
                VALUES
                ($usr_id, $msg_id, $msg_id)
                ;
            ";

            query($saw_query, NONE);
        } else {
            if( $lower != 0 && $upper != 0 ) {
                # Found "near" bounds both on the upper
                # and lower bounds.

                $update_query = '
                    UPDATE msgs_seen_bounds
                    SET
                        upper_msg_id = ' . $upper['upper_msg_id'] . '
                    WHERE
                        msgs_seen_bounds.id = ' . $lower['id'] . '
                    ;
                ';

                $delete_query = '
                    DELETE FROM msgs_seen_bounds
                    WHERE
                        msgs_seen_bounds.id = ' . $upper['id'] . '
                    ;
                ';

                query($update_query, NONE);
                query($delete_query, NONE);
            } else {
                if( $lower != 0 ) {
                    # Only found lower bound, update accordingly.
                    $update_query = '
                        UPDATE msgs_seen_bounds
                        SET
                            upper_msg_id = ' . $msg_id . '
                        WHERE
                            msgs_seen_bounds.id = ' . $lower['id'] . '
                        ;
                    ';

                    query($update_query, NONE);
                }

                if( $upper != 0 ) {
                    # Only found upper bound, update accordingly.
                    $update_query = '
                        UPDATE msgs_seen_bounds
                        SET
                            lower_msg_id = ' . $msg_id . '
                        WHERE
                            msgs_seen_bounds.id = ' . $upper['id'] . '
                        ;
                    ';

                    query($update_query, NONE);
                }
            }
        }
    } else {
        # Do nothing, already seen.
    }

}

Поиск непрочитанных сообщений - это поиск места, где current_msg_id не существует между любыми нижними и верхними значениями идентификаторов для заданный пользователь (запрос НЕ СУЩЕСТВУЕТ в терминах SQL). Это не самый эффективный из запросов при реализации в реляционной базе данных, но его можно решить с помощью агрессивной индексации. Например, ниже приведен SQL-запрос для подсчета непрочитанных сообщений для данного пользователя с группировкой по области обсуждения ("элемент"), в которой находятся сообщения:

$count_unseen_query = "
    SELECT 
        msgs.item as id,
        count(1) as the_count
    FROM msgs
    WHERE
    msgs.usr != " . $usr_id . " AND
    msgs.state != 'deleted' AND
    NOT EXISTS (
       SELECT 1 
       FROM 
          msgs_seen_bounds msb
       WHERE 
          msgs.id BETWEEN msb.lower_msg_id AND msb.upper_msg_id
          AND msb.usr_id = " . $usr_id . "
    )
    GROUP BY msgs.item
    ;

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

Учитывая небольшой форум, насчитывающий более 2000 сообщений, ниже приведена статистика использования, касающаяся количества сохраненных кортежей, отсортированных по количеству раз, когда пользователи входили в систему (приблизительно соответствует активности пользователей). Столбец "num_bounds" - это количество кортежей, необходимых для хранения истории просмотра пользователя "num_posts_read".

id  num_log_entries num_bounds num_posts_read num_posts
479             584         11           2161       228
118             461          6           2167       724
487             119         34           2093       199
499              97          6           2090       309
476              71        139            481        82
480              33         92            167        26
486              33        256            757       154
496              31        108            193        51
490              31         80            179        61
475              28        129            226        47
491              22         22           1207        24
502              20        100            232        65
493              14         73            141         5
489              14         12           1517        22
498              10         72            132        17

Я не видел этой конкретной реализации на любом форуме, кроме моего собственного, и притом небольшого. Мне было бы интересно, реализовал ли кто-нибудь еще или видел, как это реализовывалось в других местах, особенно на большом и/или активном форуме.

С уважением,

Кайден

 8
Author: Kaiden, 2011-02-27 09:51:43

Не совсем PHP-ответ, но вот как мы это делаем на нашем форуме на основе asp.net (Я связан с этим продуктом, раскрывая это в соответствии с правилами)

  1. Мы используем файлы cookie , а не базу данных.
    • Недостаток файлов cookie - не "кросс-устройство" (при посещении с другого компьютера все отображается как непрочитанное)
    • Преимущество - нет огромных операций чтения/записи БД. И отслеживание работает также для "гостевых" пользователей! Это потрясающе.
  2. Мы храним файлы cookie с парами { topicID, lastReadMessageID } для каждой темы, которую посещает пользователь.
  3. Если данные для определенной темы не найдены в файле cookie, мы предполагаем, что тема либо:
    • полностью непрочитанный (если последнее сообщение в теме больше МАКСИМАЛЬНОГО lastReadMessageID из (2)
    • полностью прочитать (если не указано иное)

В этом есть некоторые незначительные недостатки, но это делает свою работу.

ПС. Кроме того, некоторые могут сказать, что использование файлов cookie оставляет мусор на компьютер пользователя (я лично ненавижу это), но мы выяснили, что средний пользователь отслеживает около 20 тем, поэтому для каждой темы требуется около 10 байт, поэтому на жестком диске пользователя требуется менее 200 байт.

 2
Author: Alex, 2013-12-15 23:07:31

Почему вас это беспокоит?

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

Поэтому для непрочитанных потоков вы просто

Псевдокод..

$result = SELECT id,viewcount from my_forum_threads

$cache->setThreads($result['id'],$result['viewcount']);

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

Средняя страница на моем веб-сайте занимает 20 запросов mysql. Когда я кэширую, это только два до четырех запросов.

 1
Author: Layke, 2010-02-18 13:29:14

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

Итак, вы сохраняете ie. отметка времени previous_last_action и last_action в вашей таблице пользователей, last_action обновляется при каждом действии пользователя, столбец previous_last_action устанавливается один раз в last_action при входе в систему (или при создание нового сеанса - если у вас есть функция "запомнить меня"). Чтобы определить, является ли поток/сообщение непрочитанным, вы должны сравнить метку времени создания (или обновления) этого потока/сообщения со значением в previous_last_action для пользователя, который в данный момент вошел в систему.

 1
Author: wimvds, 2010-02-18 16:16:23

Быстрый ответ о том, как (я думаю) IPB это делает:

Все записи старше заданного количества (по умолчанию 30 дней) автоматически помечаются как прочитанные. Cronjob удаляет их у каждого пользователя, чтобы сохранить управляемый размер.

Все публикации возрастом менее 30 дней отслеживаются как запись JSON для каждого идентификатора пользователя + категории. Пример: 12 категорий с 1000 активными пользователями = максимум 12 000 строк.

Есть поле "количество непрочитанных" для быстрого поиска, скажем, на домашнем форуме или в любом другом месте иначе просто нужен номер.

Я мог бы полностью отключиться от фактического хранилища MySQL. Я не смог найти документацию по этому вопросу, но я покопался в базе данных и увидел таблицу, которая /выглядела/как прочитанные/непрочитанные потоки (таблица: core_item_markers, для справки). Но я положительно отношусь к гибридной модели age/mysql.

 1
Author: rannmann, 2013-07-10 17:19:57

Я прочитал все ответы, и у меня появилась идея, которая может быть лучшим сочетанием для этой темы (хотя кода нет).
Эта идея представляет собой смесь всех ваших идей и небольшого опыта, который у меня есть в программировании
Приблизительно 95 % пользователей (статистика получена от администратора форума и его журналов форума) читают темы форума прямо до последнего сообщения (или страницы) и не возвращаются, читают сообщения 1-й страницы (или только 1-й пост), а затем переходят на последнюю страницу, или они читают всю тему с последней страницы. от начала до конца, и если они повернут назад, они уже прочитали эту часть. Таким образом, хорошее решение будет работать следующим образом:
Я думаю, что если мы создадим хранилище для каждого пользователя, для каждого потока, временной метки последнего сообщения, которое просматривал пользователь (и, если применимо, первого сообщения, которое просматривал пользователь, даже если это может оказаться бесполезным), мы могли бы что-то с этим сделать. Система довольно проста и почти такая же, как у phpbb. Также было бы полезно отметить последний пост, который мы видели, чтобы продолжить в этом позже (вместо того, чтобы быть вынужденным рассматривать всю эту страницу как прочитанную). И, поскольку каждый поток имеет свой собственный идентификатор. Нет необходимости организовываться так, как это делает phpbb.

 0
Author: brunoais, 2011-02-24 13:41:18