PHP форумы - как справиться с непрочитанными дискуссиями / темами / постами - PullRequest
28 голосов
/ 18 февраля 2010

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

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

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

Ответы [ 7 ]

15 голосов
/ 22 февраля 2010

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

  1. помечает каждую ветку читателя каждым пользователем.

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

    • Недостатки: много места с «непрочитанными» строками, если большое количество пользователей неактивно
    • Решения: добавить отметку времени жизни и удалить старые записи с помощью cron
    • Преимущества: каждый пользователь знает, прочитал или нет сообщение.
  3. используйте временные метки, чтобы определить, показывать ли его как непрочитанное или нет.1029 * Недостатки: пользователи не знают, с какими реальными непрочитанными темами отмечены только «новые ветви» с момента последнего входа в систему

  4. Преимущество: экономия места

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

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

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

  • меньше места для определения непрочитанных тем

Недостатки

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

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

  • Каждый пользователь знает, какие "новые сообщения" читали или нет.
8 голосов
/ 18 февраля 2011

Есть ... другое.

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

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

[user_id, board, last_msg_id, last_timestamp]

[user_id, доска объявлений, форум, last_msg_id, last_timestamp]

[user_id, форум, форум, подфорум, last_msg_id, last_timestamp]

[user_id, форум, форум, подфорум, тема, last_msg_id, last_timestamp]

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

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

Вы храните сообщения в кортежах следующим образом: [user_id, lower_msg_id, upper_msg_id]

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

При просмотре страницы функция проверяет, есть ли у user_id запись, где current_msg_id находится между lower_msg_id и upper_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 или uppper_msg_id, то мы увеличиваем кортеж на 1 в этом направлении. Если мы не увеличиваем наш диапазон кортежей, то мы вставляем новый кортеж, [user_id, 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 не существует между любым lower_msg_id и upper_msg_id для данного пользователя (запрос NOT EXISTS в терминах 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

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

С уважением,

Кайден

2 голосов
/ 15 декабря 2013

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

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

У этого есть некоторые незначительные недостатки, но это делает работу.

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

1 голос
/ 10 июля 2013

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

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

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

Для быстрого поиска, например, на домашней странице форума или в любом другом месте, есть поле "Непрочитанное количество".

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

1 голос
/ 18 февраля 2010

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

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

1 голос
/ 18 февраля 2010

Почему вы обеспокоены?

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

Так что для непрочитанных потоков вы просто

Псевдокод ..

$result = SELECT id,viewcount from my_forum_threads

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

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

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

0 голосов
/ 24 февраля 2011

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

...