如何计算最后一秒、一分钟和一小时内的请求数?
Posted
技术标签:
【中文标题】如何计算最后一秒、一分钟和一小时内的请求数?【英文标题】:How can I count the number of requests in the last second, minute and hour? 【发布时间】:2013-07-10 03:41:52 【问题描述】:我有一个 Web 服务器,它只支持一个非常简单的 API——计算过去一小时、一分钟和一秒收到的请求数。 该服务器在世界范围内非常流行,每秒接收数千个请求。
旨在找到如何准确地将这 3 个值返回给每个请求?
请求一直在到来,因此每个请求的一小时、一分钟和一秒的窗口是不同的。 如何管理每个请求的不同窗口,以便每个请求的计数正确?
【问题讨论】:
Efficient way to compute number of hits to a server within the last minute, in real time 的可能重复项 【参考方案1】:如果需要 100% 的准确度:
拥有一个包含所有请求和 3 个计数的链接列表 - 最后一小时、最后一分钟和最后一秒。
您将有 2 个指向链接列表的指针 - 一分钟前和一秒前。
一小时前将在列表末尾。每当最后一次请求的时间比当前时间早一个多小时时,将其从列表中删除并减少小时计数。
分钟和秒指针将分别指向一分钟和一秒前发生的第一个请求。每当请求的时间比当前时间早一分钟/秒以上时,将指针上移并减少分钟/秒计数。
当有新请求进来时,将其添加到所有 3 个计数中,并将其添加到链表的前面。
对计数的请求只涉及返回计数。
以上所有操作都是摊销常数时间。
如果低于 100% 的准确度是可以接受的:
上面的空间复杂度可能有点多,具体取决于您通常每秒收到多少请求;您可以通过稍微牺牲精度来减少这种情况,如下所示:
有一个如上所述的链表,但仅限于最后一秒。也有3个计数。
然后有一个由 60 个元素组成的圆形数组,指示过去 60 秒中每一秒的计数。每当经过一秒时,从分钟计数中减去数组的最后一个(最旧的)元素,然后将最后一秒计数添加到数组中。
在过去 60 分钟内有一个类似的圆形数组。
准确性损失:一秒钟内所有请求都可以关闭分钟计数,而一分钟内所有请求都可以关闭小时计数。
如果您每秒只有一个或更少的请求,这显然是没有意义的。在这种情况下,您可以将最后一分钟保留在链表中,并且只保留最后 60 分钟的循环数组。
对此还有其他变体 - 可以根据需要调整精度与空间使用比率。
删除旧元素的计时器:
如果你只在新元素进来时移除旧元素,它将被摊销恒定时间(某些操作可能需要更长的时间,但它会平均到恒定时间)。
如果您想要真正的恒定时间,您还可以运行一个计时器来删除旧元素,并且每次调用它(当然还有插入和检查计数)只会花费恒定时间,因为您最多删除自上次计时器滴答以来以恒定时间插入的元素数量。
【讨论】:
在“100%准确率”的方法中,数组移位和递减操作的时间常数如何?通过列表进行线性扫描以删除所有旧请求将是 O(n),例如想象在 0.1 秒内有 1000 个请求的最坏情况,1 秒后您需要扫描并删除 1000 个条目,现在想象一百万个请求,您将需要扫描并删除 100 万个条目 - 所需的工作随着需要删除的请求数,不是恒定的时间。即使列表中过期请求的数量是 n 的某个恒定分数,它仍然是 O(n)。 @bain “链表”数据结构的好处是您不需要像使用简单数组和推/移那样传递所有元素来添加/删除。要删除一个元素,您可以简单地删除或重新排列“链接”。 @patmood 在链表中添加或删除节点是 O(1),但遍历链表是 O(n)。这里描述的“指针移位和计数器递减”需要遍历链表,直到我们找到第一个节点存储的请求距离当前时间不到一小时(或分钟或秒)。在最坏的情况下,这需要遍历列表中的所有 n 个节点并调用递减运算符 n 次。 bain - 无需遍历...创建一个名为 sum 的变量并添加新元素并减去旧元素.. 这就是为什么他/她的意思是 O(1) @RohitashwaNigam 当第二次过去时,您会执行我的回答中描述的更新。如果您在这个确切点检查计数,它将是准确的。但是,如果您在一秒钟的中途检查计数,那么 60 秒前的那一秒将是一个问题 - 您将不知道前半部分(应该排除)和后半部分(应该包括在内)发生了多少请求) 那一秒。所有这些都可能发生在上半场或下半场,或者在两半场发生的次数相同。当然你可以只显示准确的计数,但是会有延迟。【参考方案2】:要在 T 秒的时间窗口内执行此操作,请使用队列数据结构,您可以在其中将各个请求到达时的时间戳排入队列。当您想读取在最近的 T 秒窗口内到达的请求数时,首先从队列的“旧”端删除那些早于 T 秒的时间戳,然后读取队列的大小。您还应该在向队列添加新请求时删除元素以保持其大小有界(假设传入请求的速率有界)。
此解决方案可以达到任意精度,例如毫秒精度。如果您对返回近似答案感到满意,例如,您可以对于 T = 3600(一小时)的时间窗口,将同一秒内的请求合并到一个队列元素中,使队列大小以 3600 为界。我认为这会很好,但理论上会失去准确性。对于 T = 1,您可以根据需要在毫秒级别进行合并。
在伪代码中:
queue Q
proc requestReceived()
Q.insertAtFront(now())
collectGarbage()
proc collectGarbage()
limit = now() - T
while (! Q.empty() && Q.lastElement() < limit)
Q.popLast()
proc count()
collectGarbage()
return Q.size()
【讨论】:
【参考方案3】:为什么不直接使用循环数组呢? 我们在该数组中有 3600 个元素。
index = 0;
Array[index % 3600] = count_in_one_second.
++index;
如果你想要最后一秒,返回这个数组的最后一个元素。 如果你想要最后一分钟,返回最后 60 个元素的总和。 如果你想最后一小时,返回整个数组的总和(3600 个元素)。
他不是一个简单有效的解决方案吗?
谢谢
德里克
【讨论】:
我喜欢。使用当前时间(例如 System.currentTimeMillis())而不是索引如何? 这很简单,但我们还需要一个单独的每秒计数器。每秒之后,它应该更新 Array 元素中的值那一秒? 我喜欢这个想法,当你每秒都有连续的请求时它工作得很好,但是当你没有请求数组中的某些条目时会发生什么?如果我们减少只为最后一秒和最后一分钟提供计数器的问题,那么假设您在第 4 秒有 n 个请求,但在第 3 秒没有请求。在这种情况下,您应该为第 3 秒清理旧数据,并且通常清理回来,直到您达到秒 0 或同一分钟的条目。前面的数据清理应该只针对早于当前分钟的陈旧数据 - 1。 如果担心过时数据,我们可以有一个大小为 7200 的数组。一旦达到第 3600 个和第 7200 个元素的索引,就清理下一个 3600 个元素。【参考方案4】:一个解决方案是这样的:
1) 使用长度为 3600 的圆形数组(每小时 60 * 60 秒)保存上一小时每秒的数据。
要记录下一秒的数据,请通过移动循环数组的头指针将最后一秒的数据放入循环数组中。
2) 在循环数组的每个元素中,我们不是保存特定秒内的请求数,而是记录我们之前看到的请求数的累积和,以及一个周期的请求可以通过requests_sum.get(current_second) - requests_sum.get(current_second - number_of_seconds_in_this_period)
来计算
increament()
、getCountForLastMinute()
、getCountForLastHour()
等所有操作都可以在O(1)
时间内完成。
================================================ ============================
这是一个如何工作的示例。
如果我们在最近 3 秒内有这样的请求计数:
1st second: 2 requests
2nd second: 4 requests
3rd second: 3 requests
循环数组将如下所示:
sum = [2, 6, 9]
其中 6 = 4 + 2 和 9 = 2 + 4 + 3
在这种情况下:
1)如果要获取最后一秒的请求数(第3秒的请求数),只需计算sum[2] - sum[1] = 9 - 6 = 3
2)如果要获取最后两秒的请求数(第3秒的请求数和第2秒的请求数),只需计算sum[2] - sum[0] = 9 - 2 = 7
【讨论】:
如果几秒钟内没有请求,将如何处理场景?我们可能会错误地读取旧数据?【参考方案5】:您可以每小时创建一个大小为 60x60 的数组,并将其用作循环缓冲区。每个条目包含给定秒的请求数。当你移动到下一秒时,清除它并开始计数。当您处于数组的末尾时,您又从 0 开始,因此有效地清除了 1 小时之前的所有计数。
-
对于小时:返回所有元素的总和
分钟:返回最后 60 个条目的总和(来自 currentIndex)
对于第二个:返回当前索引的计数
所以这三个都有 O(1) 的空间和时间复杂度。唯一的缺点是,它忽略了毫秒,但您也可以应用相同的概念来包含毫秒。
【讨论】:
我们不能有滑动寡妇类型的解决方案而不是固定间隔吗?【参考方案6】:Following 代码在 JS 中。它将返回 O(1) 中的计数。我为一次面试编写了这个程序,其中时间被预先定义为 5 分钟。但是您可以修改此代码几秒钟、几分钟等。告诉我进展如何。
-
创建一个以毫秒为键,计数器为值的对象
添加一个名为 totalCount 的属性并将其预定义为 0
在步骤 1 中定义的每个命中增量计数器的日志和总计数
添加一个名为 clean_hits 的方法,每毫秒调用一次该方法
在 clean_hits 方法中,从我们创建的对象中删除每个条目(在我们的时间范围之外),并在删除条目之前从 totalCount 中减去该计数
this.hitStore = "totalCount" : 0;
【讨论】:
【参考方案7】:我必须在 Go 中解决这个问题,我认为我还没有看到这种方法,但它也可能非常适合我的用例。
由于它连接到第 3 方 API 并且需要限制自己的请求,我只是保留了最后一秒的计数器和最后 2 分钟的计数器(我需要的两个计数器)
var callsSinceLastSecond, callsSinceLast2Minutes uint64
然后,当调用计数器低于我的允许限制时,我会在单独的 go 例程中启动我的请求
for callsSinceLastSecond > 20 || callsSinceLast2Minutes > 100
time.Sleep(10 * time.Millisecond)
在每个 go 例程结束时,我会自动递减计数器。
go func()
time.Sleep(1 * time.Second)
atomic.AddUint64(&callsSinceLastSecond, ^uint64(0))
()
go func()
time.Sleep(2 * time.Minute)
atomic.AddUint64(&callsSinceLast2Minutes, ^uint64(0))
()
到目前为止,这似乎工作没有任何问题,到目前为止进行了一些相当繁重的测试。
【讨论】:
【参考方案8】:这是一个通用的 Java 解决方案,可以跟踪最后一分钟的事件数。
我使用ConcurrentSkipListSet
的原因是因为它保证了搜索、插入和删除操作的平均时间复杂度为 O(log N)。您可以轻松更改下面的代码以使持续时间(默认为 1 分钟)可配置。
正如上面的答案所建议的,定期清理过时的条目是个好主意,例如使用调度程序。
@Scope(value = "prototype")
@Component
@AllArgsConstructor
public class TemporalCounter
@Builder
private static class CumulativeCount implements Comparable<CumulativeCount>
private final Instant timestamp;
private final int cumulatedValue;
@Override
public int compareTo(CumulativeCount o)
return timestamp.compareTo(o.timestamp);
private final CurrentDateTimeProvider currentDateTimeProvider;
private final ConcurrentSkipListSet<CumulativeCount> metrics = new ConcurrentSkipListSet<>();
@PostConstruct
public void init()
Instant now = currentDateTimeProvider.getNow().toInstant();
metrics.add(new CumulativeCount(now, 0));
public void increment()
Instant now = currentDateTimeProvider.getNow().toInstant();
int previousCount = metrics.isEmpty() ? 0 : metrics.last().cumulatedValue;
metrics.add(new CumulativeCount(now, previousCount + 1));
public int getLastCount()
if (!metrics.isEmpty())
cleanup();
CumulativeCount previousCount = metrics.first();
CumulativeCount mostRecentCount = metrics.last();
if (previousCount != null && mostRecentCount != null)
return mostRecentCount.cumulatedValue - previousCount.cumulatedValue;
return 0;
public void cleanup()
Instant upperBoundInstant = currentDateTimeProvider.getNow().toInstant().minus(Duration.ofMinutes(1));
CumulativeCount c = metrics.lower(CumulativeCount.builder().timestamp(upperBoundInstant).build());
if (c != null)
metrics.removeIf(o -> o.timestamp.isBefore(c.timestamp));
if (metrics.isEmpty())
init();
public void reset()
metrics.clear();
init();
【讨论】:
【参考方案9】:简单的时间戳列表怎么样?每次发出请求时,都会将当前时间戳附加到列表中。每次你想检查你是否低于速率限制时,你首先删除超过 1 小时的时间戳以防止堆栈溢出(呵呵),然后你计算最后一秒、分钟等时间戳的数量。
这可以在 Python 中轻松完成:
import time
requestsTimestamps = []
def add_request():
requestsTimestamps.append(time.time())
def requestsCount(delayInSeconds):
requestsTimestamps = [t for t in requestsTimestamps if t >= time.time() - 3600]
return len([t for t in requestsTimestamps if t >= time.time() - delayInSeconds])
我想这可以优化,但你看到了这个想法。
【讨论】:
【参考方案10】:我的解决方案:
维护一个 3600 的哈希,其中包含一个计数、时间戳作为字段。
对于每个请求:
按时间戳%3600 获取 idx(当前元素的数组索引)。 如果 hash[idx].count=0,那么 hash[idx].count=1 并且 hash[idx].timestamp=inputTimeStamp 如果 hash[idx].count>0 ,那么案例(1) : if i/p timestamp==hash[idx].timestamp,hash[count]++;
案例(2):如果 i/p 时间戳>hash[idx].timestamp,则 hash[idx].count=1 和 hash[idx].timestamp=inputTimeStamp
Case(3): : if i/p timestamp
现在对于最后一秒、分钟、小时的任何查询: 如上找到idx,只要时间戳在给定的秒/范围/分钟内匹配,就继续循环从idx返回。
【讨论】:
以上是关于如何计算最后一秒、一分钟和一小时内的请求数?的主要内容,如果未能解决你的问题,请参考以下文章