探索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都被成 ... 
随机推荐
- EF中扩展出Between操作符 (修订版)
			随手记录一下,这是针对原文错误的修改. 原文:EF中扩展出Between操作符 直接使用是错误的,修改后的扩展方法: /// <summary> /// 扩展 Between 操作符 // ... 
- 【ajax  提交表单】多种方式的注意事项
			在业务中,可能因为表单内容过于庞大,字段过于繁杂,如果人为去拼接的话 ,需要耗费大量的时间和精力,与此同时,代码看上去也是冗余不堪. 所以,提交表单的时候如果能整个表单数据整体提交,那是非常开心的事情 ... 
- 六个漂亮的 ES6 技巧
			六个漂亮的 ES6 技巧 转载 原文:2ality 译文:众成翻译 链接:http://www.zcfy.cc/article/346 在这篇文章里,我将演示 6 种 ES6 新特性的使用技巧.在每个 ... 
- 学习android 官方文档
			9.29 1. 今天,FQ,看到android studio中文网上有一个FQ工具openVPN,我就使用了. 之前用过一个FQ工具开眼,但由于网速慢,我就弃用了. 2. 现在,我就可以FQ去andr ... 
- GRU(Gated Recurrent Unit) 更新过程推导及简单代码实现
			GRU(Gated Recurrent Unit) 更新过程推导及简单代码实现 RNN GRU matlab codes RNN网络考虑到了具有时间数列的样本数据,但是RNN仍存在着一些问题,比如随着 ... 
- PHP通过加锁实现并发情况下抢码实现
			需求:抢码功能 要求: 1.特定时间段才开放抢码: 2.每个时间段放开的码是有限的: 3.每个码不允许重复: 实现: 1.在不考虑并发的情况下实现: function get_code($len){ ... 
- JAVA_javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
			tomcat访问https请求返回: javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name at sun.se ... 
- browsersync实现网页实时刷新(修改LESS,JS,HTML时)
			var gulp = require("gulp"), less = require("gulp-less"), browserSync = require(& ... 
- C#常用类笔记
			1. Object类型转化为数组 object[] b = (object[])ArrayList.Adapter((Array)list).ToArray(typeof(object)); 
- 【转】关于FLASH中图文混排聊天框的小结
			原文链接 图文混排也是FLASH里一个很古老的话题了,我们不像美国佬那样游戏里面聊天框就是聊天框,全是文字干干净净,也不像日本人发明了并且频繁地使用颜文字.不管是做论坛.做游戏,必定要实现的一点就是带 ... 
