代码之家  ›  专栏  ›  技术社区  ›  yoda

PHP论坛-如何处理未读的讨论/主题/帖子

  •  28
  • yoda  · 技术社区  · 15 年前

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

    MyBB , vBulletin , Invision Power Board Vanilla , phpBB 等等,处理这个问题,所以我想从你们那里读一读你们的经验。我知道仅仅使用数据库表是最简单的方法,但是当社区每月有超过10000名成员和1000个新主题时,这将涉及到大量的读/写操作。这很难,但应该有办法避免服务器过载。

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

    7 回复  |  直到 14 年前
        1
  •  15
  •   Ondrej Slinták    11 年前

    没有太多的选择。

      • 优点:每个用户都知道跟帖有没有读过。
    1. 按每个用户标记每个未读线程。

      • 解决方案:添加生存期时间戳并使用cron删除旧记录
      • 优点:每个用户都知道跟帖有没有读过。
      • 缺点:用户不知道是真正的未读线程,标记只显示上次登录后的“新线程”
      • 优点:节省空间

    另一种选择是混合溶液,

    1和3)如果线程不超过X天并且没有为用户标记为已读的行,则将线程显示为“未读”。“read”行可以在它们早X天时删除,而不会产生任何影响。

    优势

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

    缺点

    • 创建一个保持系统干净的cron

    优势

    • 每个用户都知道哪些“新帖子”读过或没有读过。
        2
  •  9
  •   Kaiden    14 年前

    有。。。另一个

    另一种存储分层论坛结构(board>节>螺纹等)。它不需要a)预先填充已读/未读信息,b)在最坏的情况下不需要存储超过U*(M/2)行,其中U是用户数,M是数据库中的文章总数(通常比这个少得多)

    我前段时间研究过这个话题。我发现SMF/phpBB在存储用户阅读历史时有点“作弊”。他们的模式支持存储在给定的板、论坛、子论坛、主题(或由浏览器直接查看)中标记为已读的最后时间戳或消息ID,如下所示:

    [用户id,线路板,最后消息id,最后时间戳]

    [用户id、董事会、论坛、子论坛、主题、最后消息id、最后时间戳]

    这允许用户将特定的板、论坛、主题等标记为“已读”。但是,它需要用户的操作(或者通过阅读,或者主动单击“标记为已读”),而在phpBB的情况下,并没有给你说“我看到了这个特定的消息,你还可以看到这样一种情况:你首先阅读了某个主题的最后一条消息(查看了某个线程中的最新活动),然后你马上就被认为已经阅读了该线程的其余部分。

    SMF和phpBB可以存储这样的内容,因为很少只查看一篇文章(默认视图设置为在主题的最后一页中查看20多篇文章)。但是,对于更多线程化的论坛(尤其是一次只查看一条消息的论坛),这并不理想。如果用户读取了一个消息而不是另一个消息,那么这个系统的用户可能会关心很多,并且可能会认为只有将整个区段标记为读取时才是麻烦的,而实际上他们只希望标记为Read的一些。

    用户历史记录日志维护如下:

    在查看页面时,函数会查看用户id是否有当前消息id介于下消息id和上消息id之间的记录。如果有,则读取此页面,无需执行任何操作。如果没有,则必须发出另一个查询,这一次确定当前的\u msg \u id是小于下\u msg \u id(当前\u msg \u id==下\u msg \u id-1)还是大于上\u msg \u id(当前\u msg \u id==上\u msg \u id+1)。在这种情况下,我们将“read”或“seen”边界增长1。如果我们与lower\u msg\u id或uppper\u msg\u id相差1,那么我们将元组沿该方向增长1。如果我们不扩大元组范围,那么我们将插入一个新元组,[user\u id,current\u msg\u id,current\u msg\u id]。

    角点情况是两个元组范围彼此接近。在这种情况下,在下元组边界和上元组边界之间搜索时,通过将下元组的上边界设置为上元组的上边界来合并这两个边界,并删除上元组。

    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.
        }
    
    }
    

    搜索未读帖子就是查找给定用户的当前\u msg\u id在任何较低的\u msg\u id和较高的\u msg\u 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
        ;
    

    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
  •  3
  •   Alex from Jitbit    11 年前

    不完全是PHP的答案,但下面是我们在 asp.net-based forum (我是本产品的附属公司,根据规则披露)

    1. 我们用饼干 ,而不是数据库。
      • 缺点 cookies-而不是“跨设备”(从另一台计算机访问时,所有内容都显示为未读)
      • -没有巨大的数据库读/写。跟踪也适用于“访客”用户!这太棒了。
    2. { topicID, lastReadMessageID } 为用户访问的每个主题配对。
    3. 如果特定主题的数据 找不到 在cookie中,我们假设主题是:
      • 完全未读(如果主题的最后一条消息大于最大值 lastReadMessageID
      • 完全阅读(如果不是)

    这有一些小缺点,但它做的工作。

        4
  •  1
  •   Layke    15 年前

    你为什么担心?

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

    所以对于未读线程

    伪代码。。

    $result = SELECT id,viewcount from my_forum_threads
    
    $cache->setThreads($result['id'],$result['viewcount']);
    

    然后在页面加载时,只需获取缓存值,而不是再次查询数据库。这真的不是什么大问题。

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

        5
  •  1
  •   wimvds    15 年前

    所以你保留了上一个动作&上次\u操作时间戳在用户表中,每次用户操作都会更新上次\u操作,在登录时(或在创建新会话时-如果您具有“记住我”功能),会将上一个\u上次\u操作列设置为上次\u操作一次。要确定某个线程/消息是否未读,您需要将该线程/消息创建(或更新)时间戳与当前登录用户的上一个\u last \u操作中的值进行比较。

        6
  •  1
  •   rannmann    11 年前

    关于IPB如何(我认为)做到这一点的快速回答:

    所有早于配置量(默认30天)的帖子都会自动标记为已读。cronjob会从每个用户中删除这些内容,以保持大小可控。

    我可以完全不使用MySQL存储。我找不到这方面的文档,但我在数据库中找到了一个/看起来/像读/未读线程的表(表:core\u item\u markers,供参考)。但我对混合age/mysql模型持肯定态度。

        7
  •  0
  •   brunoais    14 年前

    我已经阅读了所有的答案,我有一个想法,可能是这个主题的最佳组合(虽然没有代码)。
    这个想法是你所有想法和我在编程方面的一点经验的结合
    大约95%的用户(统计数据来自论坛管理员和他的论坛日志)直接阅读论坛的主题到最后一个帖子(或页面),不返回,阅读第一个页面的帖子(或只是第一个帖子),然后转到最后一个页面,或者他们从头到尾阅读整条线索,如果他们回头看,他们已经读了那部分。所以一个好的解决方案是这样的: