【问题标题】:redis as write-back view count cache for mysqlredis作为mysql的回写视图计数缓存
【发布时间】:2013-05-21 15:28:08
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 大家好,所提供的答案很有启发性。我意识到其中一些可能存在问题,但我首先要对它们竖起大拇指,因为这些想法和讨论都很棒。
  • 最后,纯编码只是几行代码,“复杂性”(像往常一样)来自“假设”,即错误情况。编写实际代码会很有趣。 ;-)

标签: mysql caching redis


【解决方案1】:

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

“后台线程多久运行一次?” 要回答这个问题,您需要回答以下问题:您会丢失多少数据?数据在 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 编译来显着减少内存使用。例如,我在this answer 中描述的数据,我曾经为 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,您首先需要考虑如何访问数据,这因用户和项目而异。这里我基于这个描述采取的路线:两个消费者访问数据,一个只显示,另一个决定更新另一个数据源。

【讨论】:

  • 1.通常redis在关闭时不会丢失所有数据,但在可调整的时间间隔内写入文件// 2.通过计算我打算得到小时和分钟 - 我可以想象得到当前的小时/分钟可能是真正的工作(白天保存等)当完成数十亿次时 // 3. 我很高兴一个真正的大用例的 redis 程序员提出了与我的理论想法或多或少相同的解决方案;-)
  • 我很清楚 Redis 的持久性。我对如果它发生故障的参考不是关于 Redis 服务的简单重启。例如,如果 Redis 机器因磁盘故障而死机,这对 SLA 和数据权限有何影响?虽然可调参数存在,但它与所述顺序的更新几乎无关。如果您每秒有数千次更新,那么您要么每秒更新一次,要么花费大量时间每分钟节省数千次,并希望它不会重叠。每秒有数千次更改,您确实需要注意权衡。
【解决方案2】:

合并我的其他答案:

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

让它每小时一次,键是YYYYMMDD_HH 以便于阅读。

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

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

解决方案有两部分:

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

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

    b) 获取$ITEM的值

    b) 发送 redis-command HINCRBY hitcount_$timekey $ITEM 1

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

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

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

    c) 将每个此类哈希与当前的 hitcount_<timekey> 进行比较

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

    • 读取所有对ITEM =>计数器HGETALL $processing_key为$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。问题是您需要一个伪密钥,因为空哈希似乎会被自动删除。

【讨论】:

    【解决方案3】:

    有关答案的当前状态,请参见 EDIT3。

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

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

    • 在页面请求中添加 ITEM 的值。
    • 每分钟或 5 分钟调用一个读取 redis 键的 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。

    这意味着:被调用的页面将在 2013 年 5 月 26 日 23:37 对 ITEM1 的调用转换为 Hash 20130526_23, ITEM1。 HINCRBY count_20130526_23 ITEM1 1

    一小时后检查keys count_*的列表,直到count_20130523都被处理(通过hgetall读取key-value,更新mysql),处理后一一删除(hdel)。完成后检查 hlen 是否为 0 和 del count_...

    所以你只有少量的键(每个未处理的小时一个),这使得keys count_* 快速,然后处理该小时的操作。如果您的 cron 延迟、时间跳跃或停机一段时间或类似情况,您可以提供几个小时的 TTL。

    【讨论】:

    • 谢谢,我认为这行不通,有几十亿,所以他们可能需要在上面设置 TTL。不过,每天可能只有几百万不同。我确实喜欢“减 7”的方法,它会大大减少更新次数。
    • 然后,只要“传输到数据库”的时间,我就会做几次(10 倍)TTL。删除“未使用”条目的问题是它在删除之前增加的风险。您是指几十亿个不同的请求,还是仅仅几十亿个请求?
    • @OneSolitaryNoob:一方面是减少update- 语句的数量,另一方面是使其脱离响应时间进程。一个 cron 有很多次,甚至可能需要几秒钟。 ;-)
    • 我认为这个解决方案远非高效/正确。除非我不知道你不使用 KEYS 是如何做到的
    • 是的,“keys”命令本身非常慢,给定数量的不同页面可能需要 0.1 甚至 1 秒(这让我感到惊讶)。然而,它在用户操作之外(在 cron 中)工作,并且比所有那些单个更新语句要快得多。对于那个大数字,我也会考虑使用哈希而不是单个键。但是哈希没有 TTL!
    猜你喜欢
    • 2015-06-07
    • 2013-04-22
    • 2023-03-10
    • 2013-04-16
    • 1970-01-01
    • 2015-03-10
    • 2017-11-25
    • 2019-11-05
    • 1970-01-01
    相关资源
    最近更新 更多