【问题标题】:Paging of frequently changing data对频繁变化的数据进行分页
【发布时间】:2014-09-25 19:24:01
【问题描述】:

我正在开发一个显示“线程”列表的 Web 应用程序。该列表可以按线程拥有的喜欢的数量进行排序。一个列表中可能有数千个线程。

应用程序需要在线程之类的变化在一秒钟内变化超过 10 倍的情况下工作。此外,该应用程序分布在多个服务器上。

我想不出一种为这种列表启用分页的有效方法。而且我不能一次将整个按赞排序的列表传输给用户。

  • 一旦用户转到此列表的第 2 页,它可能会发生变化,并且可能包含已从第一页列出的话题

不起作用的解决方案:

  • 在客户端存储看到的线程(在移动设备上可能太多)
  • 将看到的线程存储在服务器端(用户和线程太多)
  • 对临时数据库表中的列表进行快照(更改数据太频繁,需要实际)

(如果重要的话,我使用的是 MongoDB+c#)

你会如何解决这类问题?

【问题讨论】:

  • 您希望达到的结果是什么?您是否正在尝试缓存结果的快照,以便用户不会尽快看到更改?似乎您可能希望使用不同的流行度指标进行排序,该指标的变化频率低于“喜欢”(或者同时拥有实时“喜欢”分数和保存的“热度”指标进行排序)。例如,一个有趣的方法是使用带有衰减的热度值(例如Drupal radioactivity module)。如果您定期计算,则排序仍然是相关的,但不会反应过度。
  • 您找到解决方案了吗?
  • 遗憾的是我从来没有想出一个正确的出路
  • 我相信几乎不可能将其命名为分页。分页 - 分成离散的页面。这个例子中的页面是什么?尝试定义它。我猜你想要实现的不是页面,而是类似于 topN 热门话题(它们可能是连续的:top10 然后 top20 等)。用户可以预期 top10 线程可能会发生变化,但她几乎不会期望第 1 页会经常更新新记录。

标签: database sorting paging


【解决方案1】:

这通常使用OLAP cube 处理。这里的想法是添加一个自然时间维度。对于此应用程序来说,它们可能太重了,但这里有一个摘要,以防其他人需要它。

OLAP 多维数据集从时间的基本概念开始。您必须知道自己关心什么时间才能理解数据。

您从“时间”表开始:

Time {
  timestamp     long      (PK)
  created       datetime
  last_queried  datetime
}

这基本上可以跟踪您的数据快照。我已经包含了一个 last_queried 字段。每当用户根据此特定时间戳请求数据时,都应使用当前时间进行更新。

现在我们可以开始讨论“线程”了:

Threads {
  id             long      (PK)
  identifier     long
  last_modified  datetime
  title          string
  body           string
  score          int
}

id 字段是一个自动递增键;这是永远不会暴露的。 identifier 是线程的“唯一”ID。我说“唯一”是因为没有唯一性约束,而且就数据库而言,它 是唯一的。那里的所有其他内容都非常标准... 除了... 当您写入时,您不会更新此条目。在 OLAP 多维数据集中,您几乎从不修改数据。更新和插入在最后解释。

现在,我们如何查询这个?你不能直接查询Threads。您需要包含一个星表:

ThreadStar {
  timestamp          long  (FK -> Time.timestamp)
  thread_id          long  (FK -> Threads.id)
  thread_identifier  long  (matches Threads[thread_id].identifier)
    (timestamp, thread_identifier should be unique)
}

该表为您提供了从现在的时间到所有线程的状态的映射。给定一个特定的时间戳,您可以通过以下方式获取线程的状态:

SELECT Thread.*
FROM   Thread
JOIN   ThreadStar ON Thread.id = ThreadStar.thread_id
WHERE  ThreadStar.timestamp = {timestamp}
   AND Thread.identifier = {thread_identifier}

这还不算太糟糕。我们如何获得线程流?首先我们需要知道现在几点。基本上你想从Time 中获取最大的timestamp 并将Time.last_queried 更新到当前时间。您可以在其前面放置一个仅每隔几秒更新一次的缓存,或者您想要的任何内容。一旦你有了它,你就可以获得所有线程:

SELECT   Thread.*
FROM     Thread
JOIN     ThreadStar ON Thread.id = ThreadStar.thread_id
WHERE    ThreadStar.timestamp = {timestamp}
ORDER BY Thread.score DESC

很好。我们有一个线程列表,随着实际分数的变化,排序是稳定的。你可以在闲暇时翻阅这个……有点。最终数据将被清理,您将丢失快照。

所以这很好,但现在你需要创建或更新一个线程。创建和修改几乎相同。两者都使用INSERT 处理,唯一的区别是您是使用现有的identifier 还是创建一个新的。

所以现在您已经插入了一个新线程。您需要更新 ThreadStar。这是疯狂昂贵的部分。基本上,您使用最新的timestamp 复制所有 ThreadStar 条目,除非您为刚刚修改的线程更新 thread_id。这是一个疯狂的重复数量。幸运的是,它几乎只是外键,但仍然如此。

你也不做DELETEs;更新 ThreadStar 时将行标记为已删除或仅将其排除。

现在您正在努力工作,但您的数据量正在疯狂增长。你可能想要清理它,除非你有很多存储预算,但即便如此,事情也会开始变慢(除此之外:这实际上会表现得非常好,即使数据量很大)。

清理非常简单。这只是一些级联删除和清理孤立数据的问题。随时从 Time 中删除条目(例如,它不是最新条目,并且 last_queried 为 null 或比任何截止值更早)。将这些删除级联到 ThreadStar。然后找到任何不在 ThreadStar 中的带有id 的线程并清理它们。

如果您有更多的嵌套数据,这种通用机制也适用,但您的查询会变得更加困难。

最后一点:由于数据量太大,您会发现插入速度非常慢。大多数地方在开发和测试环境中使用适当的约束来构建它,但随后在生产中禁用约束!

是的。确保您的测试可靠。

但至少您对分页中重新排序的数据不敏感。

【讨论】:

  • OLAP 非常适合数据仓库,但对于应用程序服务器来说,imo 太多了
【解决方案2】:

当用户第一次访问数据库时,我会将所有“线程”结果缓存在服务器上。然后将第一页数据返回给用户,对于随后的每个下一页调用,我都会返回缓存的结果。

为了最大限度地减少内存使用,您可以仅缓存记录 ID,并在用户请求时获取整个数据。

每次用户退出当前页面时都可以清除缓存。如果不是大量数据,我会坚持使用此解决方案,因为用户不会对不断变化的数据感到恼火。

【讨论】:

    【解决方案3】:

    对于像喜欢这样的不断变化的数据,我会使用两阶段方法。对于经常变化的数据,我会使用内存数据库来跟上变化率,并将其定期刷新到“真实”数据库。 一旦你有了这个,查询不断变化的数据就很容易了。

    1. 查询数据库。
    2. 查询内存数据库。
    3. 将内存数据库中频繁更改的数据与“慢”数据库数据合并。
    4. 记住您已经显示了哪些结果,因此按下下一步按钮将 不显示已经显示的值两次,因为在不同的页面上,因为它的排名已经改变。

    如果很多人查看相同的数据,它可能有助于缓存 3 本身的结果,以进一步减少真实数据库的负载。

    您当前的架构没有缓存层(站点越大,缓存的内容就越多)。如果事情变得太大,您将无法摆脱简单的数据库和对数据库的有效查询。

    【讨论】:

    • 我的问题只是你的第四点。假设我每秒有 1000 个唯一用户列表视图,并且用户可能会分页具有超过 10000 个页面(x 50 个线程)的结果集。因此,我需要每秒存储至少 50000 个“已经见过的线程”。不知道用户在页面上停留了多长时间,我需要长时间存储上次看到的线程。
    • 需求工程的一部分是处理现实数字。您当前的设计是否能够将 1K 更改/秒存储到您的数据库中?我们在谈论多少页面浏览量/秒?一台带有 IIS 的物理机可以服务于 ca。如果像 Stackoverflow (blog.cellfish.se/2014/07/…) 那样进行大量调整,则为 2000 页/秒。
    • 我有一个 MongoDB 集群设置,能够处理大约 5000 次更新/秒和 4 个前端服务器,可以处理 ~3000-4000 次请求/秒。我正在努力解决的问题是,当按经常更新的喜欢排序时,避免在客户端视图中出现重复线程。
    • 你的目标是什么?如果您想显示最新数据,那么第 4 页上显示的一个新线程可能会移动到第 1 页,因为它在这两者之间变得非常流行。或者您想在页面之间移动时及时显示快照?
    【解决方案4】:

    有趣的问题。除非我误解了你,如果我是的话,一定要告诉我,听起来最好的解决方案是实现一个系统,而不是页面 numbers,使用时间戳。这将类似于许多主要 API 已经做的事情。我知道 Tumblr 甚至在仪表板上这样做,这当然不是不合理的情况:在高峰时间的短时间内可能会添加大量帖子,具体取决于用户关注的人数。

    所以基本上,您的“下一页”按钮可以只链接到/threads/threadindex/1407051000,这可以转换为“所有在 2014 年 8 月 2 日 17:30 之前创建的线程。这使您的查询非常容易实现。然后,当您拉下所有下一个元素时,您只需查找页面上最后一个元素之前发生的任何内容。

    当然,这样做的缺点是很难知道自用户开始浏览以来添加了多少 元素,但您始终可以记录开始时间并从那时起知道任何事情会是新的。用户也很难在自己的页面中键入内容,但这在大多数应用程序中都不是问题。您还需要为线程中的每条记录存储时间戳,但这可能已经完成,如果没有,那么实现起来肯定不难。您将支付每条记录额外 8 个字节的费用,但这比存储任何关于“看到”帖子的内容要好。

    这也很好,因为这可能不适用于您,但用户可以为列表中的页面添加书签,并且它将永远保持不变,因为它与其他任何内容无关。

    【讨论】:

    • 按日期排序并实现分页不是问题,解决方案是一种时间戳分页。 (我编辑了我的主要问题以澄清)。我的问题是当你没有“常量”排序键时实现分页,因为喜欢的数量经常变化。
    • @coalmee 啊,我明白了。所以日期并不总是一种选择。这确实使事情变得更加复杂。好吧,如果您使用开始时间戳的页码混合,您认为它可以工作吗?因此,您会在该人首次加载页面时获取时间戳,然后将其放入查询中以供后续调用?这样你就不会受到他们变化如此之大的事实的影响。并且大概您在 UserLikes 表中有记录,您可以向其中添加时间戳,然后您可以构建您的排序类似计数,仅过滤用户开始浏览之前发生的那些?
    猜你喜欢
    • 1970-01-01
    • 2012-03-12
    • 2012-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多