【问题标题】:How can I improve this PHP/MySQL news feed?如何改进这个 PHP/MySQL 新闻提要?
【发布时间】:2011-05-08 21:38:30
【问题描述】:

让我从一开始就说我知道这不是最好的解决方案。我知道它很笨拙并且是一个功能的破解。 但这就是我在这里的原因!

这个问题/工作建立在some discussion on Quora with Andrew Bosworth,Facebook 新闻提要的创建者。

我正在构建一个新闻提要。它仅在 PHPMySQL 中构建。


MySQL

提要的关系模型由两个表组成。一张表用作活动日志;事实上,它被命名为activity_log。另一个表是newsfeed这些表几乎完全相同。

日志架构activity_log(uid INT(11), activity ENUM, activity_id INT(11), title TEXT, date TIMESTAMP)

...供稿的架构newsfeed(uid INT(11), poster_uid INT(11), activity ENUM, activity_id INT(11), title TEXT, date TIMESTAMP)

任何时候用户做了与新闻提要相关的事情,例如提出问题,它会立即记录到活动日志中


生成新闻提要

然后每 X 分钟(目前为 5 分钟,将更改为 15-30 分钟后),我运行一个 cron 作业,它执行下面的脚本。此脚本循环访问数据库中的所有用户,找到该用户所有朋友的所有活动,然后将这些活动写入新闻源。

目前,剔除活动的SQL(在ActivityLog::getUsersActivity() 中调用)有一个LIMIT 100,出于性能*原因。 *我不知道我在说什么。

<?php

$user = new User();
$activityLog = new ActivityLog();
$friend = new Friend();
$newsFeed = new NewsFeed();

// Get all the users
$usersArray = $user->getAllUsers();
foreach($usersArray as $userArray) {

  $uid = $userArray['uid'];

  // Get the user's friends
  $friendsJSON = $friend->getFriends($uid);
  $friendsArray = json_decode($friendsJSON, true);

  // Get the activity of each friend
  foreach($friendsArray as $friendArray) {
    $array = $activityLog->getUsersActivity($friendArray['fid2']);

    // Only write if the user has activity
    if(!empty($array)) {

      // Add each piece of activity to the news feed
      foreach($array as $news) {
        $newsFeed->addNews($uid, $friendArray['fid2'], $news['activity'], $news['activity_id'], $news['title'], $news['time']);
      }
    }
  }
}

显示新闻提要

在客户端代码中,当获取用户的新闻提要时,我会执行以下操作:

$feedArray = $newsFeed->getUsersFeedWithLimitAndOffset($uid, 25, 0);

foreach($feedArray as $feedItem) {

// Use a switch to determine the activity type here, and display based on type
// e.g. User Name asked A Question
// where "A Question" == $feedItem['title'];

}

改进新闻提要

现在请原谅我对开发新闻提要的最佳实践的有限理解,但我理解我使用的方法是所谓的写入时扇出的有限版本,仅限于感觉我正在运行一个 cron 作业作为中间步骤,而不是直接写入用户的新闻提要。但这与拉取模型有很大不同,因为用户的新闻提要不是在加载时编译,而是定期编译。

这是一个大问题,可能值得大量反复讨论,但我认为它可以作为像我这样的新开发人员需要进行的许多重要对话的试金石。我只是想弄清楚我做错了什么,我该如何改进,或者我应该如何从头开始尝试不同的方法。

让我对这个模型感到困扰的另一件事是,它是基于新近度而不是相关性来工作的。如果有人可以建议如何改进这方面的工作相关性,我会全神贯注。我正在使用 Directed Edge 的 API 来生成推荐,但对于新闻提要之类的东西,推荐器似乎不起作用(因为之前没有任何东西被收藏!)。

【问题讨论】:

  • 什么?这次没有附上幽默图片?你可以做得比这更好! :P
  • @Josh Try this :)
  • @Josh Smith 每个用户都有一个新闻源表吗?
  • @josh smith 如果您重建上述算法,请发布。谢谢
  • @JoshSmith,您是否为每个朋友执行一个 SQL 查询以获取他们的活动?

标签: php mysql web-applications feed


【解决方案1】:

我使用 2 级缓存生成用户提要的机制略有不同。 我对规模的假设是基于我的理论经验,但相同的方法可以根据要求用于不同的规模。

上图试图解释整个提要生成架构

假设您有 1 亿用户。根据 80-20 规则,20% 的活跃用户产生了 80% 的流量。考虑到每个活跃用户每天产生 20 个帖子,您有 2000 万用户每天产生 4 亿个新帖子。假设每个活跃用户有大约 1000 个朋友,其中 20% 是活跃的,即 200 个有最近帖子的活跃朋友。每个用户有(200 个活跃的朋友)*(每个用户 20 个帖子)= 4000 个帖子有资格出现在提要上。

创建一个缓存来存储 24-48 小时的最近帖子,即大约 8 亿个帖子。将这些帖子针对其所有者存储为 userid: posts[],其中用户 ID 是创建帖子的用户,帖子包含他最近 24-48 小时的帖子。

创建一个 Feed 生成器服务,该服务为每个活跃用户 (20M) 获取该用户的 200 个活跃朋友的帖子,并在 Feed 的缓存中生成另一个符合 Feed 条件的帖子数组 userid:posts[] 其中 userid 是打开他的 Feed 的用户而posts[] 是所有展示的帖子的超集。

此 Feed 生成器服务可以定期为每个活跃用户运行,也可以按需为每个不活跃用户运行。填充提要缓存后,提要生成器服务可以每隔很短的时间运行一次,以根据最近帖子缓存中的更新行填充增量帖子

Feed 服务可以连接到 Feed 的缓存并根据相关性、重要性、新近度或任何其他逻辑显示帖子。

【讨论】:

    【解决方案2】:

    不是运行 cron 作业,而是运行某种后提交脚本。我不知道 PHP 和 MySQL 在这方面的具体能力是什么——如果我没记错的话,MySQL InnoDB 允许比其他品种更高级的功能,但我不记得最新版本中是否有触发器之类的东西。

    无论如何,一个不依赖于大量数据库魔法的简单品种:

    当用户 X 添加内容时:

    1) 在数据库提交后从您的 PHP 页面执行异步调用(当然是异步的,这样查看页面的用户就不必等待它!)

    调用启动逻辑脚本的一个实例。

    2) 逻辑脚本通过提交新内容的用户的朋友列表 [A,B,C](而不是数据库中的每个人的列表!)和将用户 X 的操作附加到每个用户的提要中。

    您可以将这些提要存储为直接的 JSON 文件,并将新数据附加到每个文件的末尾。当然,最好将提要保存在缓存中,并备份到文件系统、BerkeleyDB 或 Mongo 或任何你喜欢的东西。

    这只是基于新近度而非相关性的提要的基本理念。您可以以这种方式顺序存储数据,然后在每个用户的基础上进行额外的解析以按相关性进行过滤,但这在任何应用程序中都是一个难题,并且可能不是一个匿名 Web 用户可以在没有详细信息的情况下轻松解决的问题了解您的要求;)

    jsh

    【讨论】:

      【解决方案3】:

      我正在尝试自己构建一个 Facebook 风格的新闻提要。我没有创建另一个表来记录用户的活动,而是从帖子、cmets 等的 UNION 中计算了“边缘”。

      借助一些数学知识,我使用指数衰减模型计算“边缘”,其中经过时间是自变量,考虑到 cmets、喜欢等的数量,每个帖子必须制定 lambda 常数。边缘一开始会快速下降,但几天后逐渐变平到几乎为 0(但永远不会达到 0)

      显示提要时,每条边都使用 RAND() 相乘。边缘较高的帖子会更频繁地出现

      这样,更受欢迎的帖子出现在新闻提要中的概率更高,时间更长。

      【讨论】:

      • 您没有提到 Edge 是预先计算的还是运行时计算的?
      【解决方案4】:

      真的很酷的问题。我实际上正在自己实施这样的事情。所以,我要大声思考一下。

      以下是我认为您当前实施的缺陷:

      1. 您正在处理所有用户的所有朋友,但由于同一组的人有相似的朋友,您最终会多次处理相同的用户。

      2. 如果我的一个朋友发布了某些内容,它最多不会在我的新闻提要中显示 5 分钟。而它应该立即出现,对吧?

      3. 我们正在为用户阅读整个新闻提要。我们不是只需要抓取自上次处理日志以来的新活动吗?

      4. 这不能很好地扩展。

      新闻源看起来与活动日志的数据完全相同,我会坚持使用那个活动日志表。

      如果您跨数据库分片您的活动日志,您可以更轻松地进行扩展。如果您愿意,您也可以对用户进行分片,但即使您在一张表中有 1000 万条用户记录,mysql 也应该可以进行读取。因此,无论何时查找用户,您都知道从哪个分片访问用户的日志。如果您经常归档旧日志并且只维护一组新日志,则您不必进行太多分片。或者甚至可能根本没有。如果调优得当,您可以在 MySQL 中管理数百万条记录。

      我会为您的用户表甚至日志本身利用 memcached。 Memcached 允许缓存条目最大为 1mb,如果您在组织密钥方面很聪明,您可能会从缓存中检索所有最近的日志。

      就架构而言,这将是更多的工作,但它将允许您实时工作并在未来扩展......尤其是当您希望用户开始评论在每个帖子上。 ;)

      你看到这篇文章了吗?

      http://bret.appspot.com/entry/how-friendfeed-uses-mysql

      【讨论】:

        【解决方案5】:

        在您之间可以使用用户标志和缓存。 可以说,有一个新的用户字段作为 last_activity。 每当用户输入任何活动时更新此字段。 保留一个标志,直到您获取提要的时间让我们说它是 feed_updated_on。

        现在更新函数 $user->getAllUsers();仅返回 last_activity 时间晚于 feed_updated_on 的用户。 这将排除所有没有任何活动日志的用户:)。 用户朋友的类似过程。

        您还可以使用缓存,如 memcache 或文件级缓存。

        或者使用一些 nosql 数据库将所有提要存储为一个文档。

        【讨论】:

          【解决方案6】:

          您会添加统计关键字吗?我通过分解文档正文、剥离 HTML、删除常用词和计算最常用词来制作了一个(粗略的)实现。几年前我做这个只是为了好玩(就像任何这样的项目一样,源代码已经消失了),但它适用于我的临时测试博客/论坛设置。也许它适用于您的新闻提要...

          【讨论】:

          • 3D 这实际上更容易使用像 Sphinx 这样的FULLTEXT 搜索引擎来实现,这是另一种可能的方法。我对这样的事情或@stillstanding 建议的方法的担忧是,它感觉就像是在 hack 之上的 hack。为了确定相关性,我真正想做的是计算用户与内容创建者的亲和度总和、内容类型的权重和时间衰减因子。但还不确定该怎么做……
          • 你会让它发展到什么复杂程度?这似乎是一个相当大的重量分布,但它是可行的。您必须在与年龄的相关性上添加一些对数衰减,但获取“内容类型”非常模糊。您必须设置一组关键字来匹配以确定这一点(作为一种快速解决方案。这在大规模应用程序中并不理想)。这需要一些强大的统计数据和计算机阅读能力......
          • 它可能会相当复杂;想想 Facebook 的新闻提要。但这可能需要我在此之外进行更大规模的重新思考。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-08-04
          • 2012-05-13
          • 1970-01-01
          • 1970-01-01
          • 2016-07-30
          • 2011-11-08
          • 1970-01-01
          相关资源
          最近更新 更多