探索c#之storm的TimeCacheMap
阅读目录:
概述
最近在看storm,发现其中的TimeCacheMap算法设计颇为高效,就简单分享介绍下。
思考一下如果需要一个带过期淘汰的缓存容器,我们通常会使用定时器或线程去扫描容器,以便判断是否过期从而删除。但这样性能并不友好,在数据量较大时O(n)检查是一笔不小的开销,并且在大量过期数据删除时需要频繁对容器加锁,这会多少会影响到正常的数据读写删除。
Storm设计了一种比较高效的时间缓存容器TimeCacheMap,它的算法可以在某个时间周期内将数据批量删除,一次批量删除只需要加一次锁即可,并且其读写删除复杂度均为O(1)。
算法介绍
TimeCacheMap把要缓存的数据分拆存储到多个小容器内,这里称为桶。另外有个线程专门在一定时间内去扫描这些桶,一旦发现过期后就把整个桶的数据给删除掉。 其中第二步比较关键,它并不是传统意义上的去定时扫描,而是根据过期时间来触发,比如如果一个桶过期时间10s,那么这个线程就10秒触发一次把整个桶删除即可,当然多个桶的触发策略会有所不同,但思路是同一个。
为了更详细的描述,用代码和例子介绍如下:
private LinkedList<Dictionary<K, V>> buckets;
private readonly object Obj = new object();
private static readonly int NumBuckets = ;
private Thread cleaner;
上面使用了k、v的形式作为缓存数据结构,每个Dictionary是一个桶,然后使用链表把多个桶存储起来。Obj是要锁的对象,NumBuckets是桶的数量,cleaner是清理线程。
在缓存初始化的时候,会实例三个空桶加入到buckets,清理线程开始启动循环检查,假设过期时间时30秒,桶的数量为3,当有新数据进来时,会全部加入到第一个桶中。
为了删除性能,清理线程会定期把整个桶给删除掉,一般我们会每次把链表中最后一个桶给清理掉,然后再加入一个新桶到链表头部。
这种情况下就不能按照缓存过期时间去触发线程清理了,因为有三个桶,如果每30秒触发线程清理掉最后一个桶,那么第三个桶要等到第90秒才开始清理,很明显这样是不合理的。 正确的应该是第30秒开始清理,这时就需要调整线程触发时间,比如调整成10秒,继续模拟下:
- 触发前1秒插入新数据到第一个桶,如果调整成10秒触发,等到触发删除这个桶时才过了20秒,跟缓存过期时间30秒不一致同样不合理,不管是1秒还是9秒都会导致提前删除数据,需要继续调整触发时间。
- 如上缓存提前删除不能允许的,但延迟删除一般是可以接受的,因此可以加入一些冗余时间来保证不会提前删除。 这里调整到15秒触发,触发前1秒插入的缓存桶正好在30秒后触发删除,达到不会提前删除的目的。
- 如上在触发前14秒插入数据,那就需要过了30秒+14秒才能删除。
根据上面的模拟,调整到15秒触发是一个比较合理的值,因此推出缓存最长过期时间的公式为:
expirationSecs * ( + / (numBuckets-))
如果过期时间是30秒,其最长删除时间是:
*(+/(-))=*(+0.5)=
因此其过期时间范围即为expirationSecs到expirationSecs * (1 + 1 / (numBuckets-1))之间。
清理线程
如上算法的介绍,我们在类型的构造函数中,实例化并启动清理线程:
public TimeCacheMap(int expirationSecs, int numBuckets, ExpiredCallBack ex)
{
if (numBuckets < )
throw new ArgumentException("numBuckets must be >=2");
this.buckets = new LinkedList<Dictionary<K, V>>();
for (int i = ; i < numBuckets; i++)
buckets.AddFirst(new Dictionary<K, V>());
var expirationMillis = expirationSecs * ;
var sleepTime = expirationMillis / (numBuckets - );
cleaner = new Thread(() =>
{
while (true)
{
Dictionary<K, V> dead = null;
Thread.Sleep(sleepTime);
lock (Obj)
{
dead = buckets.Last();
buckets.RemoveLast();
buckets.AddFirst(new Dictionary<K, V>());
}
if (ex != null)
ex(dead);
}
});
cleaner.IsBackground = true;
cleaner.Start();
}
代码执行步骤:
- 初始化桶加入到链表
- 计算缓存数据最长过期时间,并作为线程休眠的时间。
- 线程触发时删除最后一个桶并加入新的桶
- 不断循环休眠触发触发
- 启动线程
整个桶的数据删除只需要加一次锁即可,保证其高效。
获取、插入、删除
遍历整个链表,查询到第一个满足key的立即返回,这需要保证不会有重复key。
public V Get(K key)
{
lock (Obj)
{
foreach (var item in buckets)
{
if (item.ContainsKey(key))
return item[key];
}
return default(V);
}
}
在插入时删除对应的key,保证不会有重复的key出现。
public void Put(K key, V value)
{
lock (Obj)
{
foreach (var item in buckets)
{
item.Remove(key);
}
buckets.First().Add(key, value);
}
}
删除对应的key
public void Remove(K key)
{
lock (Obj)
{
foreach (var item in buckets)
{
if (item.ContainsKey(key))
item.Remove(key);
}
}
}
总结
在那些年我们一起追过的缓存写法(三)中有介绍过关于惰性删除及高效LRU算法优化缓存容器的过期,有兴趣的童鞋可以看看。
完整代码中有容器Size、ContainsKey的实现,github-TimeCacheMap.c#。
在storm中,spout发射的消息和acker的消息即保存在各自的TimeCacheMap里,如果消息超时后会自动通知spout的fail方法。 在storm0.8后TimeCacheMap被弃用了,使用的是新的RotatingMap,但设计和实现基本没变,github-TimeCacheMap.java及github-RotatingMap.java。
探索c#之storm的TimeCacheMap的更多相关文章
- 探索C#之系列目录导航
1. 探索c#之函数创建和闭包 2. 探索c#之尾递归编译器优化 3. 探索c#之不可变数据类型 4. 探索c#之递归APS和CPS 5. 探索c#之一致性Hash详解 6. 探索c#之微型MapRe ...
- Storm源码分析--Nimbus-data
nimbus-datastorm-core/backtype/storm/nimbus.clj (defn nimbus-data [conf inimbus] (let [forced-schedu ...
- Storm入门(九)Storm常见模式之流聚合
流聚合(stream join)是指将具有共同元组(tuple)字段的数据流(两个或者多个)聚合形成一个新的数据流的过程. 从定义上看,流聚合和SQL中表的聚合(table join)很像,但是二者有 ...
- Storm概念学习系列之storm-starter项目(完整版)(博主推荐)
不多说,直接上干货! 这是书籍<从零开始学Storm>赵必厦 2014年出版的配套代码! storm-starter项目包含使用storm的各种各样的例子.项目托管在GitHub上面,其网 ...
- Storm TimeCacheMap RotatingMap源码分析
TimeCacheMap是Twitter Storm里面一个类, Storm使用它来保存那些最近活跃的对象,并且可以自动删除那些已经过期的对象. 不过在storm0.8之后TimeCacheMap被弃 ...
- 由提交storm项目jar包引发对jar的原理的探索
序:在开发storm项目时,提交项目jar包当把依赖的第三方jar包都打进去提交storm集群启动时报了发现多个同名的文件错误由此开始了一段对jar包的深刻理解之路. java.lang.Runtim ...
- Storm
2016-11-14 22:05:29 有哪些典型的Storm应用案例? 数据处理流:Storm可以用来处理源源不断流进来的消息,处理之后将结果写入到某个存储中去.不像其它的流处理系统,Storm不 ...
- Storm 中什么是-acker,acker工作流程介绍
概述 我们知道storm一个很重要的特性是它能够保证你发出的每条消息都会被完整处理, 完整处理的意思是指: 一个tuple被完全处理的意思是: 这个tuple以及由这个tuple所导致的所有的tupl ...
- 理解storm的ACKER机制原理
一.简介: storm中有一个很重要的特性: 保证发出的每个tuple都会被完整处理.一个tuple被完全处理的意思是: 这个tuple以及由这个tuple所产生的所有的子tuple都被成 ...
随机推荐
- SQL Server 事务以及事务日志综述
事务是一个非常重要的概念,特此在这里写一些文章来总结.整篇文章还在持续更新中. 在本系列文章中,你将看到以下内容: 数据库事务(Database Transaction)概述 事务操作(BEGIN/C ...
- mac个人设置
修改spotlight快捷键 mac默认的command+space和我windows下的习惯冲突,修改为ctrl+space 删除输入法切换的快捷键 因为我不需要切换不同语言的快捷键.中英文切换直接 ...
- jQuery中事件绑定到bind、live、delegate、on方法的探究
1. 给页面上的某个元素绑定事件,最初采用下面的方式实现: $(‘selector’).click(function(){ //code }); 缺点: 不能同时绑定多个事件,不能绑定动态的元素. 后 ...
- Android获取ImageView上的图片,和一个有可能遇到的问题!
1.在获取图片前先调用setDrawingCacheEnabled(true)这个方法: 举例:mImageView.setDrawingCacheEnabled(true); 2.之后可以通过get ...
- 活动助手Beta用户试用报告
用户试用报告 1.面向参与者用户 1.1 日常参加各类学习(水综测)活动中,有没有遇到以下问题: (1) 信息来源混乱,不知道靠不靠谱 (2) 每次报名都要重新填写自己的学号手机号,有时候填错了就没综 ...
- 从零开始山寨Caffe·柒:KV数据库
你说你会关系数据库?你说你会Hadoop? 忘掉它们吧,我们既不需要网络支持,也不需要复杂关系模式,只要读写够快就行. ——论数据存储的本质 浅析数据库技术 内存数据库——STL的map容器 关 ...
- 队列的JS实现
队列和栈相似,都是对插入和删除操作的部位做了限制特殊的线性表.在队列中,只能从一头删除节点,这一头叫做队首:而另一端只能做插入操作,这一头叫做队尾.很容易理解,队列是一个"先进先出" ...
- DOS命令详解
DOS命令详解 命令 \? 可以进入命令帮助 1.md命令创建目录. MKDIR [drive:]pathMD [drive:]path 如果命令扩展被启用,MKDIR 会如下改变: 如果需要,MKD ...
- Shader实例:溶解效果(Dissolve)
效果: 图左:一道金光闪过,瞬间灰飞烟灭 图右:燃烧效果,先过渡到黄色,然后渐渐过渡到黑色,最后消失殆尽. 这是游戏中常见的效果,各位可以想想自己玩过的游戏. 手头正在玩的,梦三国手游,死亡的时候就是 ...
- python列表副本
a=[1,2,3] b=[4,5,6] a=a+b #创建含a和b的副本的新列表 a [1, 2, 3, 4, 5, 6] b [4, 5, 6] c=a+b #创建含a和b的副本的新列表c [1, ...