基本问题很简单: 每次发生事件时都有一个运行的插入列表(让我们说一下文件的下载次数) - 想法是保持已经下载的次数的运行总数但是到期从那时起24小时内每次加入运行总计(因此总数本身不应该重置为零,而是超过24小时的下载应该淡出它)。
(另外,如果有{... 1}} increment
行动自动过期,有人请立即打扰我。
特殊情况是,有一个小应用程序已经使用Redis
来跟踪一些重复更新的值,Redis
使用MongoDB
进行长期存储。我即将对某些功能实施每日配额,可以在Pymongo
或Redis
中跟踪。
Redis有一个问题,它基本上是平的,所以为了跟踪每个下载过期(而不是总数),必须创建单独的项目: 即
MongoDB
然后,为了确定下载次数是否已达到每日限制,我可以计算由" user1.votes.action _ *'标识的缓存中的对象数。
我认为第二种选择是将投票放入带有时间戳的pymongo集合中 - 然后忽略项目,其中cache.set('filename1.downloads.action_234612', {'downloads': 1)
cache.expire('filename1.downloads.action_234612', 86400)
(不仅仅因为MongoDB已被使用而不是SQL)。
我知道在较低级别上执行此操作的速度可能要快得多 - 但我不确定性能差异是否足以证明其合理性。 (至少在概念上似乎可以在不索引项集的情况下这样做,并且仅在堆栈的最上面的项上操作)。
请注意,如果服务器出现故障或其他情况,则会在开始时运行完整性检查,并重新计算文件下载的次数(或跟踪的任何增量)。
答案 0 :(得分:1)
我无法告诉您使用MongoDB或Redis是否会更好,但这里是我如何使用Redis。
对于每个计数器,即下载的文件,保留一个有序集。已排序集的成员应表示下载操作,其分数是操作的时间戳。更新已排序的集合时,会从中修剪旧项目并设置整个集合的到期时间。
向计数器添加下载的工作流程将类似于伪Python中的以下内容(请注意,redis-py会切换成员和分数的顺序):
r.zadd('filename1:downloads', 'action_234612', time.time())
r.zremrangebyscore('filename1:downloads', '-inf', time.time()-86400)
r.expire('filename1:downloads', 86400)
您可能想要使用上面的MULTI / EXEC块,或者使用服务器端Lua脚本。
现在已经完成了艰苦的工作,获取文件的当前下载次数(即在过去24小时内)只需要ZCOUNT
关键(您也可以在此处修剪它) ):
downloads = r.zcount('filename1:downloads', time.time()-86400, time.time())
答案 1 :(得分:1)
缺乏声誉意味着我无法对Itamar完全有效的答案发表评论。
最近做过类似的事情,我只对前一个解决方案进行了两处小改动。 当将一个项目添加到有序集合时,我(并且似乎其他人)也不需要成员项目,并且最初通过使用唯一的计数器项目实现类似于Itamar的解决方案。
过了一会儿,我把它换成了:
r.zadd('filename1:downloads', time.time(), time.time())
使成员和时间戳具有相同(唯一)的值。
当用户尝试启动操作时,检查操作是否超过配额时,我确保修剪(zremrangebyscore)覆盖了我感兴趣的整个时间窗口(86400)并使用了zcard ()而不是zcount()。
再次使用伪代码:
def try_download(r, sorted_set_key, timestamp=time.time(), limit=1000, window=24 * 60 * 60):
# trim current set
r.zremrangebyscore(sorted_set_key, '-inf', timestamp - window)
# how many items are there in the set?
count = r.zcard(sorted_set_key)
# too many?
if count >= limit:
return False
# add new download
r.zadd(sorted_set_key, timestamp, timestamp)
# expire after window seconds
r.expire(sorted_set_key, window)
# return True meaning download allowed
return True
zcard和zadd之间存在竞争条件,可以通过WATCH / MULTI / EXEC或LUA脚本来解决。
答案 2 :(得分:0)
主要使用pandas.Series.shift的版本。它强加了1分钟的时间分辨率,但可以推广到任何时间分辨率。
它依靠大小为整个时间窗口的日记帐。对于24小时的时间范围,可以(24 * 60的值即1440)。但是对于较大的时间范围,可能会占用相当多的内存(30天为43200个值)...
每次创建记录时都会转移日志,以忘记太旧的记录。
from datetime import datetime
import numpy as np
import pandas as pd # must be at least version 0.24
class QuotaExceededError(Exception): pass
class TimeWindowQuota():
"""
Check if a quota has not been exceeded during a past given time window.
IMPORTANT: the time window resolution is in minute. So any two records
occuring within the same minute of the current date will be counted as
one record.
"""
def __init__(self, time_window_minutes, limit):
"""
Args:
- time_window_minutes is a positive integer
- limit is a number above which to raise QuotaExceededError
"""
self.limit = limit
self.journal = pd.Series(np.zeros(time_window_minutes))
self.head_date = None # will be initialize at 1st record
def record(self, quantity=1, date=None):
"""
Record a quantity for a given date. Quantities recorded outside the past
time window will be forgotten.
IMPORTANT: the time window resolution is in minute. So any two records
occuring within the same minute of the current date will be counted as
one record.
Args:
- quantity : a number to record (can be negative)
- date (datetime): date associated with the recording.
Cannot be a date earlier than the previous call of record.
Default is datetime.now().
"""
if date is None:
date = datetime.now()
if self.head_date is not None:
assert(date >= self.head_date) # cannot record in the past
dt = (date - self.head_date).total_seconds() / 60.0
self.journal = self.journal.shift(int(dt), fill_value=0.0)
self.journal.loc[0] += quantity
if self.journal.sum() >= self.limit:
raise QuotaExceededError()
self.head_date = date
if __name__ == '__main__':
tracker = TimeWindowQuota(time_window_minutes=24*60, limit=100)
tracker.record(quantity=90, date=datetime(2020,4,24,20,20,0))
tracker.record(quantity=50, date=datetime(2020,4,25,20,20,0))
tracker.record(quantity=20, date=datetime(2020,4,26,10,50,0))
tracker.record(quantity=29, date=datetime(2020,4,26,20,19,0))
# This will raise a QuotaExceededError:
tracker.record(date=datetime(2020,4,26,20,19,0))