8

我有一个吞吐量非常高的站点,我试图在 mySQL 数据库中存储每个页面的“查看次数”(由于遗留原因,它们最终必须以 mySQL 结尾)。

视图的绝对数量使得执行 SQL“UPDATE ITEM SET VIEW_COUNT=VIEW_COUNT+1”类型的语句变得不切实际。有数以百万计的项目,但大多数只被查看了少量次,其他的被查看了很多次。

所以我正在考虑使用 Redis 来收集视图计数,并使用一个后台线程将计数写入 mySQL。这样做的推荐方法是什么?该方法存在一些问题:

  • 后台线程多久运行一次?
  • 它如何确定要写回 mySQL 的内容?
  • 我应该为每个被击中的项目存储一个 Redis KEY 吗?
  • 我应该使用什么 TTL?
  • 是否已经有一些预先构建的解决方案或PowerPoint演示文稿让我走到了一半,等等。

我在 StackOverflow 上看到了非常相似的问题,但没有一个很好的答案……但是!希望此时有更多的 Redis 知识。

4

3 回答 3

7

我认为你需要退后一步,从不同的角度审视你的一些问题,才能得到答案。

“后台线程多久运行一次?” 要回答这个问题,您需要回答以下问题:您会丢失多少数据?数据在 MySQL 中的原因是什么,以及该数据的访问频率是多少?例如,如果每天只需要查询一次数据库以获取报告,那么您可能只需要每天更新一次。另一方面,如果 Redis 实例死了怎么办?您可以损失多少增量并且仍然“正常”?这些将提供关于多久更新一次 MySQL 实例的问题的答案,我们无法为您回答。

我会使用一种非常不同的策略将其存储在 redis 中。为了便于讨论,让我们假设您决定需要每小时“刷新到 db”。

将每个命中存储在哈希中,并按照以下方式使用键名结构:

interval_counter:DD:HH
interval_counter:total

使用页面 id(例如 URI 的 MD5 总和、URI 本身或您当前使用的任何 ID)作为哈希键并在页面视图上执行两次增量;每个哈希一个。这为您提供了每个页面的当前总数和要更新的页面子集。

然后,您将让您的 cron 作业在一小时开始后运行一分钟左右,以通过抓取前一小时的哈希来拉下所有具有更新视图计数的页面。这为您提供了一种非常快速的方法来获取数据以更新 MySQL 数据库,同时避免任何需要做数学运算或玩时间戳等技巧。通过从不再增加的键中提取数据,您可以避免由于竞争条件时钟偏斜。

您可以在每日密钥上设置过期时间,但我宁愿在成功更新数据库后使用 cron 作业将其删除。这意味着如果 cron 作业失败或无法执行,您的数据仍然存在。它还通过不变的键为前端提供一整套已知的命中计数器数据。如果您愿意,您甚至可以保留每日数据,以便能够对页面的受欢迎程度进行窗口查看。例如,如果您通过 cron 作业而不是删除设置过期,将每日哈希值保留 7 天,您可以显示上周每个页面每天的流量。

执行两个 hincr 操作可以单独或流水线完成,仍然表现得相当好,并且比在代码中进行计算和处理数据更有效。

现在是关于过期低流量页面与内存使用的问题。首先,您的数据集听起来不像需要大量内存的数据集。当然,这在很大程度上取决于您如何识别每个页面。如果您有一个数字 ID,则内存需求将相当小。如果你仍然有太多的内存,你可以通过配置调整它,如果需要,甚至可以使用 32 位的 redis 编译来显着减少内存使用。例如,我在这个答案中描述的数据是我曾经为 Internet 上十个最繁忙的论坛之一管理的,它消耗的数据不到 3GB。我还将计数器存储在比我在这里描述的更多的“时间窗口”键中。

也就是说,在这个用例中,Redis 是缓存。如果在上述选项之后您仍然使用太多内存,您可以设置密钥过期并为每个 ht 添加过期命令。更具体地说,如果您遵循上述模式,您将在每次点击时执行以下操作:

hincr -> total
hincr -> daily
expire -> total

这使您可以通过在每次访问时延长其过期时间来保持正在使用的任何内容的新鲜度。当然,要做到这一点,您需要包装您的显示调用以在总计哈希上捕获 hget 的空答案并从 MySQL DB 填充它,然后递增。您甚至可以将两者都作为增量进行。如果您的 Redis 节点需要重新填充,这将保留上述结构,并且可能是从 MySQL Db 更新 Redis 服务器所需的相同代码库。为此,您需要考虑并决定哪个数据源将被视为权威。

您可以通过根据您从前面的问题中确定的数据完整性参数修改间隔来调整 cron 作业的性能。要获得更快运行的 cron nob,请减小窗口。使用这种方法减小窗口意味着您应该有一个较小的页面集合来更新。这里的一大优势是您无需弄清楚需要更新哪些密钥然后去获取它们。您可以执行 hgetall 并迭代哈希的键以进行更新。这还通过一次检索所有数据来节省许多往返行程。在任何一种情况下,如果您可能想要考虑从属于第一个 Redis 实例的第二个 Redis 实例来进行读取。您仍然可以对 master 执行删除操作,但这些操作更快,并且不太可能在您的写入繁重的实例中引入延迟。

如果您需要 Redis DB 的磁盘持久性,那么肯定将其放在从属实例上。否则,如果您确实经常更改大量数据,您的 RDB 转储将不断运行。

我希望这会有所帮助。没有“固定”的答案,因为要正确使用 Redis,您首先需要考虑如何访问数据,这因用户和项目而异。在这里,我基于此描述采取的路线:两个消费者访问数据,一个仅显示,另一个确定更新另一个数据源。

于 2013-05-27T19:39:55.807 回答
3

合并我的其他答案:

定义从 redis 到 mysql 的传输应该发生的时间间隔,即分钟、小时或天。以某种方式定义它,以便可以快速轻松地获得识别密钥。这个键必须是有序的,即更小的时间应该给出更小的键。

让它每小时一次,关键是YYYYMMDD_HH可读性。

定义一个前缀,如“hitcount_”。

然后对于每个时间间隔,您hitcount_<timekey>在 redis 中设置一个哈希,其中包含该间隔的所有请求项目,形式为 ITEM => count。

解决方案有两个部分:

  1. 必须计算的实际页面:

    a) 获取当前的$timekey,即按日期函数

    b) 获取值$ITEM

    b) 发送 redis-commandHINCRBY hitcount_$timekey $ITEM 1

  2. 一个在给定时间间隔内运行的 cronjob,不太接近该时间间隔的限制(例如:不是整小时)。这个 cronjob:

    a) 提取当前时间键(现在是 20130527_08)

    b)从redis请求所有匹配的键KEYS hitcount_*(应该是一个小数字)

    c) 将每个这样的散列与当前散列进行比较hitcount_<timekey>

    d)如果该密钥小于当前密钥,则将其处理为$processing_key

    • 读取所有对 ITEM => counter by HGETALL $processing_keyas $item, $cnt
    • 使用 `UPDATE ITEM SET VIEW_COUNT=VIEW_COUNT+$cnt where ITEM=$item" 更新数据库
    • 从哈希中删除该键HDEL $processing_key $item
    • 无需删除散列本身 - 据我尝试,redis 中没有空散列

如果您想涉及 TTL,例如 cleanup-cronjob 可能不可靠(可能不会运行很多小时),那么您可以通过具有适当 TTL 的 cronjob 创建未来的哈希,这意味着现在我们可以创建一个 TTL 10 小时的哈希 20130527_09,TTL 11 小时的 20130527_10,TTL 12 小时的 20130527_11。问题是您需要一个伪密钥,因为空散列似乎会被自动删除。

于 2013-05-27T06:37:33.297 回答
1

有关 A...nswer 的当前状态,请参见 EDIT3。

我会为每个项目写一个密钥。几万把钥匙绝对没有问题。

页面变化很大吗?我的意思是你得到很多永远不会再被调用的页面吗?否则我会简单地:

  • 在页面请求中添加 ITEM 的值。
  • 每分钟或 5 分钟调用一个读取 redis-keys 的 cronjob,读取值(比如 7)并通过 decrby ITEM 7 减少它。在 MySQL 中,您可以将该 ITEM 的值增加 7。

如果您有很多永远不会被再次调用的页面/项目,您可以每天进行一次清理工作以删除值为 0 的键。这应该被锁定以防止从网站再次增加该键。

我根本不会设置 TTL,所以这些值应该永远存在。您可以检查内存使用情况,但我看到许多具有当前 GB 内存的不同可能页面。

编辑: incr 对此非常好,因为如果之前未设置,它会设置密钥。

EDIT2:鉴于大量不同的页面,您可以使用 HASHES 和 incrby (http://redis.io/commands/hincrby),而不是缓慢的“keys *”命令。我仍然不确定 HGETALL 是否比 KEYS * 快得多,并且 HASH 不允许单个键的 TTL。

EDIT3:哦,好吧,有时好主意来晚了。这很简单:只需在键前面加上一个时间段(比如一天一小时),或者创建一个名为“requests_”的 HASH。这样就不会发生删除和增量的重叠!每小时您使用旧的“day_hour_*”值获取可能的键,更新 MySQL 并删除那些旧键。唯一的条件是您的服务器在时钟上没有太大差异,因此请使用 UTC 和同步服务器,并且不要在 x:01 而在 x:20 左右启动 cron。

That means: a called page converts a call of ITEM1 at 23:37, May 26 2013 to Hash 20130526_23, ITEM1. HINCRBY count_20130526_23 ITEM1 1

One hour later the list of keys count_* is checked, and all up to count_20130523 are processed (read key-value by hgetall, update mysql), and deleted one by one after processing (hdel). After finishing that you check if hlen is 0 and del count_...

So you only have a small amount of keys (one per unprocessed hour), that makes keys count_* fast, and then process the actions of that hour. You can give a TTL of a few hours, if your cron is delayed or timejumped or down for a while or something like that.

于 2013-05-26T17:47:13.223 回答