PHP论坛 - 如何应对未读的讨论/主题/帖子

yod*_*oda 28 php mysql forum

我知道这个问题曾在这里被问过几次,但没有一个答案让我高兴.这是因为几乎所有这些都涉及与数据库相关的巨大读/写过程,我想不惜一切代价避免这种过程.

关于未读的讨论/主题/帖子,有很多想法.我不知道像MyBB,vBulletin,Invision Power Board,Vanilla,phpBB等论坛系统如何处理这个问题,所以我想向你们看一下你们的经验.我知道使用数据库表是最简单的方法,但当社区每月有超过10,000名成员和1000个新主题时,这将涉及巨大的读/写.这很难,但应该有办法避免服务器的重载.

那么,您认为这个问题的最佳实践是什么,以及其他论坛系统如何应对它?

use*_*ess 15

没有太多选择.

  1. 每个用户标记每个读者线程.

    • 缺点:非常活跃的论坛中有很多行
    • 优点:每个用户都知道帖子有没有读过.
  2. 标记每个用户的每个未读线程.

    • 缺点:如果很多用户不活动,很多空间都会出现"未经过重复"的行
    • 解决方案:添加生命周期时间戳并使用cron删除旧记录
    • 优点:每个用户都知道帖子有没有读过.
  3. 使用时间戳来确定是否将其显示为未读.

    • 缺点:用户不知道是真正的未读线程,标记只显示自上次登录后的"新头衔"
    • 优势:节省空间

另一种选择是混合解决方案,即

1和3)如果线程不超过X天并且没有标记为用户的行,则将线程显示为"未读"."读取"行可以在X日龄时删除而不会影响任何内容.

好处

  • 较小的间隔用于确定未读线程

缺点

  • 创建一个保持系统清洁的cron
  • 用户不知道他们是否读取了超过x天的线程.

好处

  • 每个用户都知道哪些"新帖子"已经阅读过.


Kai*_*den 8

还有另一种.

另一种存储分层论坛结构(板>部分>线程等)的详细读/未读数据的方法.它没有a)必须预先填充读/未读信息,和b)在最坏的情况下不必存储超过U*(M/2)行,其中U是用户数,而M是数据库中的帖子总数(通常很多,比这少得多)

我刚才研究过这个话题.我发现SMF/phpBB在他们如何存储用户阅读历史方面"欺骗"了一点.他们的模式支持存储最后一个时间戳或在给定的板,论坛,子论坛,主题(或直接由浏览器查看)中标记为已读的消息ID,如下所示:

[user_id,board,last_msg_id,last_timestamp]

[user_id,board,forum,last_msg_id,last_timestamp]

[user_id,board,forum,subforum,last_msg_id,last_timestamp]

[user_id,board,forum,subforum,topic,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)小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.
    }

}
Run Code Online (Sandbox Code Playgroud)

搜索未读帖子是查找给定用户的任何lower_msg_id和upper_msg_id之间不存在current_msg_id的位置(SQL术语中的NOT EXISTS查询).在关系数据库中实现时,它不是最有效的查询,但可以通过积极的索引来解决.例如,以下是用于计算给定用户的未读帖子的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
    ;
Run Code Online (Sandbox Code Playgroud)

在论坛上阅读的用户越多,每个元组标记为读取的边界越宽,并且必须存储的元组越少.用户可以获得准确的读取与未读取的计数,并且可以非常容易地聚合以在每个论坛,子论坛,主题等中查看读取与未读取.

鉴于一个约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
Run Code Online (Sandbox Code Playgroud)

我没有在任何论坛中看到这个特定的实现,而是我自己的自定义实现,并且它是一个小的.如果其他人已经实施了,或者在其他地方实现了这一点,我会感兴趣,特别是在大型和/或活跃的论坛中.

问候,

Kaiden