我想实现一个特殊的计数器:所有增量操作在一段固定的时间(比如30天)后超时。
一个例子:
天真的实现是维护一组时间戳,其中每个时间戳等于增量的时间。减去所有已超时的时间戳后,计数器的值等于该组的大小。
这个天真的计数器有O(n)空间(集合的大小),有O(n)查找和O(1)插入。这些值是准确的。
准确性的交易速度和记忆。
我想要一个带O(1)查找和插入O(1)空间的计数器。精度<精确。
或者,我会接受O(log n)空格并查找。
计数器表示应适合存储在数据库字段中,即,我应该能够快速更新和轮询计数器,而不会产生太多(反)序列化开销。
我实际上是在寻找一个类似于HyperLogLog计数器的计数器,但是对于不同类型的近似计数:衰减增量与不同元素的数量
我怎么能实现这样的计数器?
答案 0 :(得分:4)
如果您可以使用24小时粒度,那么您可以将计数器装入k桶中,其中k是最长TTL中的天数。
递增是一个O(1)操作 - 只需使用索引(k-TTL)增加存储桶中的值,以及当前的总和。
读取是另一个O(1)操作,因为您只需读取当前总和。
cronjob每晚都会从现在过期的桶中弹出(并在另一端添加一个值为0的桶)并将该计数器减少该桶中的总和(这是一个后台任务,因此它不会影响您的插入或阅读操作)
答案 1 :(得分:3)
这是一个基于退火的计数器(在Python中实现)。
class AnnealingCounter():
def __init__(self, alpha=0.9):
self.alpha = alpha # rate of decay
self.last_t = .0 # time of last increment
self.heat = .0 # value of counter at last_t
def increment(self, t=None, amount=1.0):
"""
t is a floating point temporal index.
If t is not provided, the value of last_t is used
"""
if t is None: t = self.last_t
elapsed = t - self.last_t
if elapsed < .0 :
raise ValueError('Cannot increment the counter in the past, i.e. before the last increment')
self.heat = amount + self.heat * (self.alpha ** elapsed)
self.last_t = t
def get_value(self, t=None):
"""
t is a floating point temporal index.
If t is not provided, the value of last_t is used
"""
if t is None: t = self.last_t
elapsed = t - self.last_t
if elapsed < .0 :
raise ValueError('Cannot increment the counter in the past, i.e. before the last increment')
return self.heat * (self.alpha ** elapsed)
def __str__(self):
return str('Counter value at time {}: {}'.format(self.last_t, self.heat))
def __repr__(self):
return self.__str__()
算法准确w.r.t。替代配方(退火与TTL)。它有O(1)增量和读数。它消耗O(1)空间,实际上只有三个浮点字段。
>>> c = AnnealingCounter(alpha=0.9)
Counter has value 0.0 at time 0.0
>>> c.increment() # increment by 1.0, but don't move time forward
Counter has value 1.0 at time 0.0
>>> c.increment(amount=3.2, t=0.5) # increment by 3.2 and move time forward (t=0.5)
Counter has value 4.14868329805 at time 0.5
>>> c.increment() # increment by 1.0, but don't move time forward
Counter has value 5.14868329805 at time 0.5
>>> c.get_value() # get value as after last increment (t=0.5)
5.148683298050514
>>> c.get_value(t=2.0)
4.396022866630942 # get future value (t=2.0)
以下是如何使用它:
declare List<String> intrestedIn= new ArrayList<String>();
答案 2 :(得分:1)
由于增量按照它们发生的顺序到期,因此时间戳形成一个简单的队列。
计数器的当前值可以单独存储在 O(1)附加存储器中。在每个操作开始时(插入或查询),当队列的前面过期时,它会从队列中弹出,并且计数器会减少。
请注意,每个 n 时间戳都会创建并弹出一次。因此,您有 O(1)分配时间来访问当前值, O(n)内存来存储未过期的时间戳。实际最高内存使用量也受新时间戳插入的TTL /频率比限制。