尝试3:时间桶设计方案
你应该已经注意到,前面的两个实现都有一个小bug。我们用time_t来保存时间戳,它保存的是一个以秒为单位的整数。因为这个近似,所以MinuteCount()实际上返回的是介于59~60秒钟的结果,根据调用它的时间而不同。
例如,如果一个事件发生在time=0.99秒,这个time会近似成t=0秒。如果你在time=60.1秒调用MinuteCount(),它会返回t=1,2,3……60的事件的总和。因此会遗漏第一个事件,尽管它从技术上来讲发生在不到一分钟以前。
平均来讲,MinuteCount()会返回相当于59.5秒的数据。并且HourCount()会返回相当于3 599.5秒的数据(一个微不足道的误差)。
可以通过使用亚秒粒度来修正这个误差。但是有趣的是,大多数使用MinuteHourCounter的应用程序不需要这种程度的精度。我们会利用这一点来设计一个新的MinuteHourCounter,它要快得多并且占用的空间更少。它是在精度与物有所值的性能之间的一个平衡。
这里的关键思想是把一个小时间窗之内的事件装到桶里,然后用一个总和累加这些事件。例如,过去1分种里的事件可以插入60个离散的桶里,每个有1秒钟宽。过去1小时里的事件也可以插入60个离散的桶里,每个1分钟宽。
如图一样使用这些桶,方法MinuteCount()和HourCount()的精度会是1/60,这是合理的。[1]
如果要更精确,可以使用更多的桶,以使用更多内存为交换。但重要的是这种设计使用固定的、可预知的内存。
实现时间桶设
如果只用一个类来实现这个设计会产生很多错综复杂的代码,很难理解。相反,我们会按照第11章中的建议,创建一些不同的类来处理问题的不同部分。
一开始,首先创建一个不同的类来保存一个时间段里的计数(如最后一小时)。把它命名为TrailingBucketCounter。它基本上是MinuteHourCounter的泛化版本,用来处理一个时间段。以下是接口:
你可能会想为什么Add()和TrailingCount()需要当前时间(time_t now)来做参数——如果用这些方法自己来计算当前的time()不是更方便吗?
尽管这看上去有点怪,但传入当前时间有两个好处。首先,它让TrailingBucketCounter成为一个“时钟无关”的类,一般来讲这更容易测试而且会避免bug。其次,它把所有对time()的调用保持在MinuteHourCounter中。对于时间敏感的系统,如果能把所有获得时间的调用放在一个地方会有帮助。
假设TrailingBucketCounter已经实现了,那么MinuteHourCounter就很容易实现了:
这段代码更容易读,也更灵活——如果我们想增加桶的数量(通过增加内存使用来改善精度),那将会很容易。
[1]与前面的方案相似,最后一只桶平均只有实际的一半。用这种设计,我们可以用保持61只桶而不是60只桶并且忽略当前“正在进行中”的桶来进行补救。但是会让数据有点部分“堆积”。一个更好的修正是把当前正在进行的桶与最老的桶里的一个互补部分结合,得到一个既无偏差又最新的计数。这种实现留给读者作为练习。
实现TrailingBucketCounter
现在所有剩下的工作就是实现TrailingBucketCounter类了。再一次,我们会创建一个辅助类来进一步拆分这个问题。
我们会创建一个叫做ConveyorQueue的数据结构,它的工作是处理其下的计数与总和。TrailingBucketCounter类可以关注根据过去了多少时间来移动ConveyorQueue。
下面是ConveyorQueue接口:
假设这个类已经实现了,请看TrailingBucketCounter多么容易实现:
现在它拆成两个类(TrailingBucketCounter和ConveyorQueue),这是第11章所讨论的又一个例子。我们也可以不用Convey or Queue,直接把所有的东西存放在TrailingBucketCounter里。但是这样代码更容易理解。
实现ConveyorQueue
现在剩下的只是实现ConveyorQueue类:
现在我们完成了!我们有一个又快又能有效地使用内存的MinuteHourCounter,外加一个更灵活的TrailingBucketCounter,它很容易重用。例如,很容易就能创建一个功能更齐全的RecentCounter来计算更多时间间隔范围,比如过去一天或者过去十分钟。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论