Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

Существует не так много вариантов.

  • отметьте каждый поток читателей каждым пользователем.

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

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

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

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

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

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

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

Недостатки

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

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

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

Ответ 2

Существует... другое.

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

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

[user_id, board, last_msg_id, last_timestamp]

[user_id, board, forum, 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

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

Привет,

Кайден

Ответ 3

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

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

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

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

Ответ 4

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

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

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

Псевдокод..

$result = SELECT id,viewcount from my_forum_threads

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Существует поле "непрочитанное количество" для быстрого поиска, например, для Forum Home или где-либо еще, просто требуется число.

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

Ответ 7

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