跟踪每日配额(自动到期增量) - Redis还是Pymongo?

时间:2014-07-27 23:31:41

标签: python mongodb redis

基本问题很简单: 每次发生事件时都有一个运行的插入列表(让我们说一下文件的下载次数) - 想法是保持已经下载的次数的运行总数但是到期从那时起24小时内每次加入运行总计(因此总数本身不应该重置为零,而是超过24小时的下载应该淡出它)。

(另外,如果有{... 1}} increment行动自动过期,有人请立即打扰我。

特殊情况是,有一个小应用程序已经使用Redis来跟踪一些重复更新的值,Redis使用MongoDB进行长期存储。我即将对某些功能实施每日配额,可以在PymongoRedis中跟踪。

Redis有一个问题,它基本上是平的,所以为了跟踪每个下载过期(而不是总数),必须创建单独的项目: 即

MongoDB

然后,为了确定下载次数是否已达到每日限制,我可以计算由" user1.votes.action _ *'标识的缓存中的对象数。

我认为第二种选择是将投票放入带有时间戳的pymongo集合中 - 然后忽略项目,其中cache.set('filename1.downloads.action_234612', {'downloads': 1) cache.expire('filename1.downloads.action_234612', 86400) (不仅仅因为MongoDB已被使用而不是SQL)。

我知道在较低级别上执行此操作的速度可能要快得多 - 但我不确定性能差异是否足以证明其合理性。 (至少在概念上似乎可以在不索引项集的情况下这样做,并且仅在堆栈的最上面的项上操作)。

请注意,如果服务器出现故障或其他情况,则会在开始时运行完整性检查,并重新计算文件下载的次数(或跟踪的任何增量)。

3 个答案:

答案 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))