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

时间:2010-02-18 13:15:49

标签: php mysql forum

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

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

那么,您认为这个问题的最佳实践是什么,以及其他论坛系统如何处理它?<​​/ p>

7 个答案:

答案 0 :(得分:15)

没有太多选择。

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

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

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

    • 缺点:用户不知道是真正的未读线程,标记仅显示自上次登录后的“新头衔”
    • 优势:节省空间
  4. 另一种选择是混合解决方案,即

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

    优点

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

    缺点

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

    优点

    • 每个用户都知道哪些“新帖子”已阅读过。

答案 1 :(得分:8)

还有......另一个。

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

我刚才研究过这个话题。我发现SMF / phpBB&#34;欺骗&#34;他们如何存储用户阅读历史。他们的架构支持存储最后一个时间戳或在给定的板,论坛,子论坛,主题(或直接由浏览器查看)中标记为已读的消息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]

这可以让用户将特定的主板,论坛,主题等标记为&#34; read&#34;。但是,它要求用户采取任何行动(通过阅读,或主动点击&#34;标记为读取&#34;),对于phpBB,不会给你粒度说&#34;我已经看到了这个特定的消息,但没有看到那个特定的消息。&#34;您还会遇到首先阅读主题中的最后一条消息(查看主题中的最新活动)的情况,并且您立即假定已阅读其余主题。

适用于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)多一个。这就是我们发展我们的阅读&#34;或&#34;见过&#34;如果我们离一个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.
    }

}

搜索未读帖子是查找给定用户的任何lower_msg_id和upper_msg_id之间不存在current_msg_id的位置(SQL术语中的NOT EXISTS查询)。在关系数据库中实现时,它不是最有效的查询,但可以通过积极的索引来解决。例如,以下是一个SQL查询,用于计算给定用户的未读帖子,按帖子所在的讨论区域(&#34; item&#34;)进行分组:

$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多个帖子的小型论坛,以下是关于存储的元组数量的使用统计信息,按用户登录的次数排序(近似用户活动)。列&#34; num_bounds&#34;是存储用户&#34; num_posts_read&#34;所需的元组数量。观看历史。

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

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

此致

Kaiden

答案 2 :(得分:2)

不完全是一个PHP答案,但这是我们在asp.net-based forum(我隶属于此产品,根据规则披露)的方式。

  1. 我们使用cookies ,而不是数据库。
    • 缺点的Cookie - 不是“跨设备”(从另一台计算机访问显示所有内容都未读)
    • 优势 - 没有巨大的数据库读/写。跟踪也适用于“访客”用户!这太棒了。
  2. 我们为用户访问的每个主题存储{ topicID, lastReadMessageID }对的Cookie。
  3. 如果cookie中找不到特定主题的数据,我们假设主题是:
    • 完全未读(如果主题的最后一条消息大于MAX lastReadMessageID来自(2)
    • 完全阅读(否则)
  4. 这有一些小缺陷,但它确实起作用。

    PS。此外,有些人可能会说使用cookie会在用户的计算机上留下垃圾(我个人讨厌这个),但我们发现一般用户跟踪大约20个主题的顶部,因此每个主题需要大约10个字节,所以它需要不到200个字节在用户的硬盘上。

答案 3 :(得分:1)

你为什么这么关心?

我没有看到任何用于获取未读线程的I / O的问题。它不必是活的。基于缓存值的15分钟延迟将起作用。

因此,对于未读线程,您只需

伪代码..

$result = SELECT id,viewcount from my_forum_threads

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

然后在页面加载时,您只需获取缓存值,而不是再次查询数据库。它根本不是一个大问题。

我网站上的平均页面需要20个mysql查询。当我缓存时,它只有两到四个查询。

答案 4 :(得分:1)

我所知道的几乎所有论坛都会使用某种参考时间戳来确定线程/消息是否应该被视为“未读”。此时间戳通常是您上次访问论坛时执行的最后一次操作的日期/时间。

所以你保持ie。一个previous_last_action&amp;在用户表中的last_action时间戳,last_action在每个用户操作上更新,在登录时(或在创建新会话时 - 如果您有“记住我”功能),previous_last_action列设置为last_action。要确定线程/消息是否未读,您可以将该线程/消息创建(或更新)时间戳与当前登录用户的previous_last_action中的值进行比较。

答案 5 :(得分:1)

快速回答IPB如何(我认为):

所有超过配置金额(默认为30天)的帖子都会自动标记为已读。 cronjob从每个用户修剪这些以保持大小可管理。

所有不到30天的帖子都会被跟踪为每个用户ID +类别的JSON条目。例如:12个类别,1000个活跃用户=最多12,000行。

有一个&#34;未读数&#34;用于快速查找论坛主页的字段,或其他任何地方只需要一个数字。

我可以完全关闭实际的MySQL存储空间。我无法找到关于此的文档,但是我挖掘了数据库,看到了一个/看起来/像是读/未读线程的表(table:core_item_markers,供参考)。但我对混合时代/ mysql模型持肯定态度。

答案 6 :(得分:0)

我已经阅读了所有的答案,并且我提出了一个可能是这个主题的最佳组合的想法(尽管没有代码)。
这个想法融合了你所有的想法和我在编程方面的一点经验 Aprox 95%的用户(统计数据来自论坛管理员和他的论坛日志)直接读到论坛的主题到最后一个帖子(或页面),不要回去,阅读第1页的帖子(或只是第1篇帖子) )然后转到最后一页,或者他们从头到尾阅读整个帖子,如果他们回头,他们已经阅读了那部分。所以一个好的解决方案就是这样的:
我想如果我们为每个用户为每个线程创建一个商店,那么用户查看的最后一个帖子的时间戳(如果适用的话,用户查看的第一篇帖子,即使这可能没有用),我们也可以得到在某个地方。系统非常简单,几乎就像phpbb一样。标记我们之后看到的最后一篇文章也是有用的(而不是被迫将所有页面视为已阅读)。并且,因为每个线程都有自己的id。没有必要像phpbb那样组织。