我知道这个问题曾在这里被问过几次,但没有一个答案让我满意。这是因为几乎所有这些都涉及与数据库相关的巨大读/写过程,我想不惜一切代价避免这种过程。
关于未读的讨论/主题/帖子,有很多值得思考的问题。我不知道像MyBB,vBulletin,Invision Power Board,Vanilla,phpBB等论坛系统如何处理这个问题,所以我我想从你们这里读到你们的经验。我知道使用数据库表是最简单的方法,但当社区每月有超过10,000名成员和1000个新主题时,这将涉及巨大的读/写。这很难,但应该有办法避免服务器的重载。
那么,您认为这个问题的最佳实践是什么,以及其他论坛系统如何处理它?</ p>
答案 0 :(得分:15)
没有太多选择。
标记每个用户的每个读者线程。
标记每个用户的每个未读线程。
使用时间戳来确定是否将其显示为未读。
另一种选择是混合解决方案,即
1和3)如果线程不超过X天并且没有为用户标记为readed的行,则将线程显示为“未读”。 “读取”行可以在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(我隶属于此产品,根据规则披露)的方式。
{ topicID, lastReadMessageID }
对的Cookie。lastReadMessageID
来自(2)这有一些小缺陷,但它确实起作用。
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那样组织。