我有一个非常高吞吐量的站点,我正试图为mySQL数据库中的每个页面存储“查看计数”(由于遗留原因,它们最终必须以mySQL结束)。
大量的观点使得执行SQL“UPDATE ITEM SET VIEW_COUNT = VIEW_COUNT + 1”类型的语句变得不切实际。有数百万个项目,但大多数只被观看了很少次,其他项目被多次观看。
所以我正在考虑使用Redis来收集视图计数,后台线程将计数写入mySQL。这样做的推荐方法是什么?该方法存在一些问题:
我在StackOverflow上看到了非常类似的问题,但没有一个有很好的答案......希望此时有更多的Redis知识。
答案 0 :(得分: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来减少内存使用量。例如,我在this answer中描述的数据,我过去常常管理互联网上十个最繁忙的论坛之一,它消耗的数据少于3GB。我还将计数器存储在更多的时间窗口中#34;钥匙比我在这里描述的要好。
也就是说,在这个用例中,Redis是缓存。如果在上述选项之后仍然使用太多内存,则可以设置密钥到期并为每个ht添加expire命令。更具体地说,如果您按照上述模式进行操作,则每次点击都会执行以下操作:
hincr -> total
hincr -> daily
expire -> total
这使您可以通过每次访问时延长它的到期时间来保留积极使用的新内容。当然,为此,您需要打包显示调用以捕获总计哈希上的hget的空答案,并从MySQL数据库中填充它,然后递增。你甚至可以做两个增量。这将保留上述结构,如果Redis节点需要重新填充,则可能与从MySQL Db更新Redis服务器所需的代码库相同。为此,您需要考虑并决定哪些数据源将被视为具有权威性。
您可以根据您从早期问题中确定的数据完整性参数修改间隔来调整cron作业的性能。要获得更快速运行的cron nob,请减少窗口。使用此方法减少窗口意味着您应该有一个较小的页面集合来更新。这里的一大优势是您不需要弄清楚需要更新哪些密钥然后再获取它们。你可以做一个hgetall并迭代哈希的密钥来做更新。通过一次检索所有数据,这也可以节省许多往返次数。在任何一种情况下,如果您可能想要考虑第二个Redis实例从属于第一个进行读取。您仍然会对主服务器执行删除操作,但这些操作会更快,并且不太可能在写入繁重的实例中引入延迟。
如果你需要Redis DB的磁盘持久性,那么肯定把它放在一个从属实例上。否则,如果您确实经常更改大量数据,则RDB转储将持续运行。
我希望有所帮助。没有"罐头"答案是因为要正确使用Redis,首先需要考虑如何访问数据,这在用户和项目与项目之间存在很大差异。在这里,我基于此描述所采用的路线:两个消费者访问数据,一个仅显示,另一个确定更新另一个数据源。
答案 1 :(得分:2)
巩固我的另一个答案:
定义从redis到mysql的传输时间间隔,即分钟,小时或日。以某种方式定义它,以便快速,轻松地获得识别密钥。必须订购此密钥,即较小的时间应该给出较小的密钥。
让它成为每小时,密钥为YYYYMMDD_HH
以便于阅读。
定义类似“hitcount _”的前缀。
然后,对于每个时间间隔,您在redis中设置哈希hitcount_<timekey>
,其中包含该区间的所有请求项,格式为ITEM =&gt;计数。
解决方案有两个部分:
必须计算的实际页面:
a)获取当前$timekey
,即按日期函数
b)获取$ITEM
b)发送redis-command HINCRBY hitcount_$timekey $ITEM 1
在给定间隔内运行的cronjob,不要太接近该间隔的限制(例如:不是整个小时)。这个cronjob:
a)提取当前时间键(目前为20130527_08)
b)从redis请求所有匹配的密钥KEYS hitcount_*
(那些应该是一个小数字)
c)将每个此类哈希与当前hitcount_<timekey>
d)如果该密钥小于当前密钥,则将其处理为$processing_key
:
HGETALL $processing_key
计数为$ item,$ cnt HDEL $processing_key $item
如果你想要一个TTL,比如说清理cronjob可能不可靠(可能不运行很多小时),那么你可以通过cronjob用适当的TTL创建未来的哈希值,这意味着现在我们可以创建一个哈希20130527_09,ttl 10小时,20130527_10,TTL 11小时,20130527_11,TTL 12小时。问题是你需要伪装,因为空哈希似乎会被自动删除。
答案 2 :(得分:1)
请参阅EDIT3了解A ... nswer的当前状态。
我会为每个ITEM写一把钥匙。几千个钥匙绝对没问题。
页面变化很大吗?我的意思是你会得到很多永远不会再被调用的页面吗?否则我会简单地说:
如果你有很多页面/ ITEMS永远不会被再次调用你可以每天做一次清理工作来删除值为0的键。这应该被锁定以防止再次从网站上增加该键。
我根本不会设置任何TTL,因此这些值应该永远存在。您可以检查内存使用情况,但我看到许多不同的可能页面具有当前GB内存。
编辑:incr非常好,因为它设置了密钥,如果之前没有设置。
EDIT2:鉴于大量不同的页面,您可以使用带有incrby(http://redis.io/commands/hincrby)的HASHES而不是慢的“keys *”命令。我还不确定HGETALL是否比KEYS *快得多,并且HASH不允许单键的TTL。
编辑3:哦,有时好的想法来得晚。它非常简单:只需在密钥前面添加一个时间段(比如一天 - 小时),或者创建一个名为“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读取键值,更新mysql),并在处理后逐个删除(hdel) 。完成后,检查hlen是否为0并且del count _...
因此,您只有少量密钥(每个未处理的小时一个),这使得keys count_*
快速,然后处理该小时的操作。如果你的cron被延迟或者跳了一段时间或者下降了一段时间或类似的东西,你可以提供几个小时的TTL。