我知道这个问题曾在这里被问过几次,但没有一个答案让我高兴.这是因为几乎所有这些都涉及与数据库相关的巨大读/写过程,我想不惜一切代价避免这种过程.
关于未读的讨论/主题/帖子,有很多想法.我不知道像MyBB,vBulletin,Invision Power Board,Vanilla,phpBB等论坛系统如何处理这个问题,所以我想向你们看一下你们的经验.我知道使用数据库表是最简单的方法,但当社区每月有超过10,000名成员和1000个新主题时,这将涉及巨大的读/写.这很难,但应该有办法避免服务器的重载.
那么,您认为这个问题的最佳实践是什么,以及其他论坛系统如何应对它?
use*_*ess 15
没有太多选择.
每个用户标记每个读者线程.
标记每个用户的每个未读线程.
使用时间戳来确定是否将其显示为未读.
另一种选择是混合解决方案,即
1和3)如果线程不超过X天并且没有标记为用户的行,则将线程显示为"未读"."读取"行可以在X日龄时删除而不会影响任何内容.
好处
缺点
好处
还有另一种.
另一种存储分层论坛结构(板>部分>线程等)的详细读/未读数据的方法.它没有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
| 归档时间: |
|
| 查看次数: |
6728 次 |
| 最近记录: |