下面,就介绍一下这个缓存类的实现过程及原理。
实现缓存的类的核心思路介绍:
1:用static Dictionary<string,object> 来存档。
A:为了处理并发,V4.0或以上,可以用System.Collections.Concurrent.ConcurrentDictionary<string,object> 来存档。
B:如果为了支持.NET 2.0,则需要自己实现一个加锁的字典(本文即此种情况)
2:对该Dictionary提供增删改查方法。
3:提供定时缓存的过期策略。
4:提供文件监控策略。
5:测试并发、性能、和内存占用问题。
以下内容,重点介绍我的思路,源码截图以片断方式提供,具体的源码,会在链接中。
1:自定义线程安全的MDictionary(支持.NET 2.0)
如果要支持2.0,那么就只能自己实现了:实现的思路也很简单,只要对操作都加上锁即可:
详情源码见:https://github.com/cyq1162/cyqdata/blob/master/Tool/MDictionary.cs
2:时间过期策略:
1 2 | private MDictionary< string , object > theCache = new MDictionary< string , object >(2048, StringComparer.OrdinalIgnoreCase); //key,cache private MDictionary< string , DateTime> theKeyTime = new MDictionary< string , DateTime>(2048, StringComparer.OrdinalIgnoreCase); //key,time |
有了theKeyTime,在每取get cache的时候,根据时间可以判断出,该Key是不是,如果已过期,则放弃。
但是有一个问题,如果缓存已经过期,但一直不被调用,那不是一直存在?
为了解决这个问题,需要一个定时器,定时清理过期的Cache。
由于Cache已经被设计成单例,因此可以在构造函数启动一个线程,来做定时任务清理过期的缓存。
下面有两种策略,以前的,和现在的,我分别介绍一下:
以前的:
定时遍历theKeyTime,找到过期时间的Cache进行删除。
因为遍历期间集合不能修改或删除,因此将遍历的符合条件的存档到新的对象,再统一处理新的对象去清除。
优点:逻辑简单。
缺点:遍历的过程,缓存不能被修改,需要锁住(缓存的对象越多,锁住的时间越长),另外每次都要遍历所有。
现在的:
1 | private SortedDictionary< int , MList< string >> theTime = new SortedDictionary< int , MList< string >>(); //worktime,keylist |
新增加了一个时间片字典,以固定的时间(如5分钟)为1个单位。
这样所有缓存的时间就有序的分散在这些时间片上,定时器只要按节奏处理一个就可以了。
每个时间片都记录所有的Key。
缺点:增加处理逻辑。
优点:过期策略不再有锁,能快速直接定位过期数据并清除。
3:关于List的性能
【一开始我的思路是List<key> keys来存档所有key,移除的时候只移除key,然后其它交给定时器去清理。
由于只考虑它是线程安全,结果做性能测试时,很明显的发现问题】
List是链表实现,因此,随着数据量的增加,Contains方法的性能会极速下降。
因此,需要简单的处理一下解决性能问题,临时折腾了个MList:
internal class MList<T>
{
List<T> list;
Dictionary<T, int> dic;
public MList()
{
list = new List<T>();
dic = new Dictionary<T, int>();
}
public MList(int num)
{
list = new List<T>(num);
dic = new Dictionary<T, int>(num);
}
public void Add(T key)
{
dic.Add(key, 0);
list.Add(key);
}
public bool Contains(T key)
{
return dic.ContainsKey(key);
}
public void Remove(T key)
{
dic.Remove(key);
list.Remove(key);
}
public void Clear()
{
dic.Clear();
list.Clear();
}
public int Count
{
get
{
return list.Count;
}
}
public List<T> GetList()
{
return list;
}
}
4:文件缓存依赖策略:
这个简而言之,就是文件被修改的时候,如何使缓存自动过期。
我要支持这个策略的原因:是因为Taurus.MVC,对View加载的html会被缓存在内存中的,当html被修改时,需要及时反应清掉缓存并重新加载。
private MDictionary<string, string> theFileName = new MDictionary<string, string>();//key,filename private MDictionary<string, FileSystemWatcher> theFolderWatcher = new MDictionary<string, FileSystemWatcher>();//folderPath,watch private MDictionary<string, MList<string>> theFolderKeys = new MDictionary<string, MList<string>>();//folderPath,keylist
重点讲解:
1:用FileSystemWatcher来做文件监控(发现.NET Core里竟然有支持这个类)
2:问题:一开始,也是想的很简单,每一个文件开一个监控就完事了,结果没那么简单:
1 2 3 | A:FileSystemWatcher对象太多,性能下降很快。 B:不同的Key指向同一个路径问题。 |
3:解决:后来,想到监控是以文件夹为单位,那么通过文件夹来搞搞实现:
1 2 3 | A:以文件夹为单位:因此,文件对象即可以减少很多,提升性能问题。 B:以文件夹为单位:可以汇总对应的Keys,当文件变更时,可以快速定位到文件。 |
5:并发:
一个缓存类写好后,测试是少不了的,特别是并发,毕竟缓存是属于高并发的操作。
因此,缓存哪些地方要加lock的,哪些可以不加的,都需要仔细思考。
测试是通过的,就不截图了。
6:性能:
性能测试,是通过和HttpRuntime.Cache做的比较。
100万次的插入:
100万次的移除:
7:占用内存:
暂无测试。
详细源码:
https://github.com/cyq1162/cyqdata/blob/master/Cache/LocalCache.cs