带你走进缓存世界(4):缓存之缓
2011-08-04 13:05
465 查看
缓存二字,从字面上分为两块:“缓”与“存”。上节我们提到的缓存原理,其实是在讲的一个“存”字,如何存取。大致回顾下是key对应的hashcode,根据hashcode作为数组下标来存取,因为存在hash冲突,速度虽达不到O(1),但也是非常之快。今天就说下“缓”的策略。
缓,便意味着“暂时”的意思,过一段时间就不再存在或被替换掉了,所以我们要说的其实是缓存的过期策略。在缓存入门篇中,主要提到了Cache类的Insert的方法,其中的几个变化的参数寓意着各种缓存策略,有具体依赖的有按时间的,一一来看。
按过期时间缓存
这种缓存策略最为简单,只要判断当前时间是否超过了指定的过期时间就remove掉该缓存项即可,一般用于不影响大碍的数据,比如论坛帖子列表,热门板块会更新极其频繁,缓存起来最为合适。但是又不能不更新缓存,不然有人发帖和回帖就看不到了,但可以缓存个一两分钟,两分钟后自动过期,重新加载新的列表,这样就不用管了,所以这种缓存策略更倾向于“不用管”的缓存。既然如此,那么我们就自己写一个按时间过期的缓存类吧。下面的这个类非常基础:
view
plain
/// <summary>
/// 按时间缓存类
/// </summary>
public class CacheByDateTime<TKey,TValue>
{
/// <summary>
/// 内部缓存项
/// </summary>
class CacheItem
{
/// <summary>
/// 缓存的值
/// </summary>
public TValue value { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public DateTime dateTime { get; set; }
}
/// <summary>
/// 缓存数据词典
/// </summary>
private readonly Dictionary<TKey, CacheItem> _dict;
/为了线程安全,需要对dict的操作加锁
private static readonly object LockDict = new object();
public CacheByDateTime()
{
_dict = new Dictionary<TKey, CacheItem>();
}
/// <summary>
/// 添加一个缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="dateTime">过期时间</param>
public void Add(TKey key, TValue value, DateTime dateTime)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict[key].value = value;
_dict[key].dateTime = dateTime;
}
else
{
_dict.Add(key, new CacheItem { value = value, dateTime = dateTime });
}
}
}
/// <summary>
/// 获取缓存
/// </summary>
public TValue Get(TKey key)
{
if (_dict.ContainsKey(key))
{
var val = _dict[key].value;
//判断缓存项是否过期
if (_dict[key].dateTime > DateTime.Now)
{
return val;
}
else
{
Remove(key);
return val;//这里可以酌情是否返回Value,因为毕竟可以省去一次查询
}
}
return default(TValue);
}
/// <summary>
/// 移除缓存
/// </summary>
public void Remove(TKey key)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict.Remove(key);
}
}
}
}
按间隔时间缓存
这个相对上面的绝对过期时间来说更有趣一些,他的策略是只要被访问,就延迟该缓存的绝对过期时间(间隔时间比如是5分钟就延长5分钟)。这种过期策略似乎十分精明,但对缓存的数据类型也是极其讲究,这种策略一般来缓存什么合适呢?如果说缓存永不过期的数据最为合适,但不存在这样的数据,像网站的配置这种数据极少改动,但访问量巨大,如果用这种缓存策略,不管管理员怎么修改配置,估计这缓存都是更新不了了,反而用上面的缓存合适,而像文章内容这种数据,访问的随机性比较大,拿捏不准啥时候过期,但文章内容极少会被更新,而网站的访问量基本上又属内容页比较大,所以这种缓存缓存文章内容比较合适。可以有效的延长热门内容的过期时间,而冷门的文章自然而言就自动过期了。具体的代码实现只需要在上面的类的Add方面做些改动就可实现:
view
plain
/// <summary>
/// 添加一个缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="timeSpan">间隔时间</param>
public void Add(TKey key, TValue value, TimeSpan timeSpan)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict[key].value = value;
_dict[key].dateTime.Add(timeSpan);
}
else
{
_dict.Add(key, new CacheItem { value = value, dateTime = DateTime.Now.Add(timeSpan) });
}
}
}
依赖项缓存
依赖缓存相对以上两个来说是非常复杂的处理过程,比如文件依赖,会有相应的监测程序(FileMonitor)来管理dependency对象。这里我们便不讲解,了解其用处即可,着实因为太过复杂。有兴趣的可以看.Net源码。
LRU(Least Recently Used)缓存
从名字便知其意,其主要用于限定容量(比如内存大小或缓存数量)的缓存,需要在缓存容器满了之后踢出过期缓存的策略,是使用次数最少或很久没使用的缓存项策略。
实现原理一般使用链表方式把所有缓存项连起来,每当有新的缓存进入则把缓存放入链表前端,如果缓存被使用则把他提到链表前端,那么没被使用的将慢慢趋于链表后端,所以当容量满了以后,就优先移除链表末尾的缓存项。当然,也有其他更为复杂的过期策略,比如同时使用缓存时间。虽然此策略和上面的按时间间隔延长缓存有点相像,但这个更侧重于缓存容器大小的管理,毕竟内存是有限的,此策略多用于公共缓存服务。下面的类是个简单的LRU实现,只限定的缓存的长度并没有大小限制,如果要做大小限制则需要计算每一个value的大小。
view
plain
/// <summary>
/// LRUCache
/// </summary>
public class LRUCache<TKey,TValue>
{
/// <summary>
/// 缓存项
/// </summary>
class CacheItem
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public CacheItem Left { get; set; }
public CacheItem Right { get; set; }
public CacheItem(TKey key, TValue value)
{
Key = key;
Value = value;
}
}
private readonly static object LockDict = new object();
private readonly IDictionary<TKey, CacheItem> _dict;
public int Length { get; private set; }
public LRUCache(int maxLength)
{
_dict = new Dictionary<TKey, CacheItem>();
Length = maxLength;
}
//链表头部
private CacheItem _first;
//链表末端
private CacheItem _last;
public bool HasKey(TKey key)
{
return _dict.ContainsKey(key);
}
/// <summary>
/// 添加一个缓存项
/// </summary>
public void Add(TKey key, TValue value)
{
var item = new CacheItem(key, value);
lock (LockDict)
{
//如果没有缓存项,则item既是first也是last
if (_dict.Count == 0)
{
_last = _first = item;
}
//如果只有一个缓存项,则item是first,first和last变为last
else if (_dict.Count == 1)
{
_last = _first;
_first = item;
_last.Left = _first;
_first.Right = _last;
}
else
{
//item为first,之前的前端向后移位
item.Right = _first;
_first.Left = item;
_first = item;
}
//如果超过的链表长度
if (_dict.Count >= Length)
{
//断开last并移除
_last.Left.Right = null;
_dict.Remove(_last.Key);
_last = _last.Left;
}
//将item放入dict
if (_dict.ContainsKey(key))
_dict[key] = new CacheItem(key, value);
else
_dict.Add(key, new CacheItem(key, value));
}
}
/// <summary>
/// 获取一个缓存项
/// </summary>
public TValue Get(TKey key)
{
if (!_dict.ContainsKey(key))
{
return default(TValue);
}
var item = _dict[key];
lock (LockDict)
{
if (_dict.Count == 1)
{
return item.Value;
}
//如果item左侧有缓存项,则将左侧的缓存指向item的右侧
if (item.Left != null)
{
item.Left.Right = item.Right;
}
else
{
//否则说明item是first
return item.Value;
}
//如果item右侧有缓存项,则将右侧的缓存指向item的左侧
if (item.Right != null)
{
item.Right.Left = item.Left;
}
else
{
//否则说明item是last
//将last的左侧的右侧断开,让其成为last
_last.Left.Right = null;
_last = _last.Left;
}
//断开item的左侧,让item成为first,让first成为item的右侧项
item.Left = null;
item.Right = _first;
_first.Left = item;
_first = item;
}
return item.Value;
}
public void Remove(TKey key)
{
if (!_dict.ContainsKey(key))
{
return;
}
var item = _dict[key];
lock (LockDict)
{
//如果item左侧有值,则将左侧的右侧指向item的右侧
if (item.Left != null)
{
item.Left.Right = item.Right;
}
else
{
//否则item则是first,所以将item的右侧赋值给first
_first = item.Right;
}
//如果item的右侧有值,则将item的右侧的左值指向item的左侧
if (item.Right != null)
{
item.Right.Left = item.Left;
}
else
{
_last = item.Left;
}
_dict.Remove(key);
}
}
}
以上提到的是我们常用的几种缓存策略,当然还有其他的策略,我们后面也会提到。今天就先到这吧。
缓,便意味着“暂时”的意思,过一段时间就不再存在或被替换掉了,所以我们要说的其实是缓存的过期策略。在缓存入门篇中,主要提到了Cache类的Insert的方法,其中的几个变化的参数寓意着各种缓存策略,有具体依赖的有按时间的,一一来看。
按过期时间缓存
这种缓存策略最为简单,只要判断当前时间是否超过了指定的过期时间就remove掉该缓存项即可,一般用于不影响大碍的数据,比如论坛帖子列表,热门板块会更新极其频繁,缓存起来最为合适。但是又不能不更新缓存,不然有人发帖和回帖就看不到了,但可以缓存个一两分钟,两分钟后自动过期,重新加载新的列表,这样就不用管了,所以这种缓存策略更倾向于“不用管”的缓存。既然如此,那么我们就自己写一个按时间过期的缓存类吧。下面的这个类非常基础:
view
plain
/// <summary>
/// 按时间缓存类
/// </summary>
public class CacheByDateTime<TKey,TValue>
{
/// <summary>
/// 内部缓存项
/// </summary>
class CacheItem
{
/// <summary>
/// 缓存的值
/// </summary>
public TValue value { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public DateTime dateTime { get; set; }
}
/// <summary>
/// 缓存数据词典
/// </summary>
private readonly Dictionary<TKey, CacheItem> _dict;
/为了线程安全,需要对dict的操作加锁
private static readonly object LockDict = new object();
public CacheByDateTime()
{
_dict = new Dictionary<TKey, CacheItem>();
}
/// <summary>
/// 添加一个缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="dateTime">过期时间</param>
public void Add(TKey key, TValue value, DateTime dateTime)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict[key].value = value;
_dict[key].dateTime = dateTime;
}
else
{
_dict.Add(key, new CacheItem { value = value, dateTime = dateTime });
}
}
}
/// <summary>
/// 获取缓存
/// </summary>
public TValue Get(TKey key)
{
if (_dict.ContainsKey(key))
{
var val = _dict[key].value;
//判断缓存项是否过期
if (_dict[key].dateTime > DateTime.Now)
{
return val;
}
else
{
Remove(key);
return val;//这里可以酌情是否返回Value,因为毕竟可以省去一次查询
}
}
return default(TValue);
}
/// <summary>
/// 移除缓存
/// </summary>
public void Remove(TKey key)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict.Remove(key);
}
}
}
}
按间隔时间缓存
这个相对上面的绝对过期时间来说更有趣一些,他的策略是只要被访问,就延迟该缓存的绝对过期时间(间隔时间比如是5分钟就延长5分钟)。这种过期策略似乎十分精明,但对缓存的数据类型也是极其讲究,这种策略一般来缓存什么合适呢?如果说缓存永不过期的数据最为合适,但不存在这样的数据,像网站的配置这种数据极少改动,但访问量巨大,如果用这种缓存策略,不管管理员怎么修改配置,估计这缓存都是更新不了了,反而用上面的缓存合适,而像文章内容这种数据,访问的随机性比较大,拿捏不准啥时候过期,但文章内容极少会被更新,而网站的访问量基本上又属内容页比较大,所以这种缓存缓存文章内容比较合适。可以有效的延长热门内容的过期时间,而冷门的文章自然而言就自动过期了。具体的代码实现只需要在上面的类的Add方面做些改动就可实现:
view
plain
/// <summary>
/// 添加一个缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="timeSpan">间隔时间</param>
public void Add(TKey key, TValue value, TimeSpan timeSpan)
{
lock (LockDict)
{
if (_dict.ContainsKey(key))
{
_dict[key].value = value;
_dict[key].dateTime.Add(timeSpan);
}
else
{
_dict.Add(key, new CacheItem { value = value, dateTime = DateTime.Now.Add(timeSpan) });
}
}
}
依赖项缓存
依赖缓存相对以上两个来说是非常复杂的处理过程,比如文件依赖,会有相应的监测程序(FileMonitor)来管理dependency对象。这里我们便不讲解,了解其用处即可,着实因为太过复杂。有兴趣的可以看.Net源码。
LRU(Least Recently Used)缓存
从名字便知其意,其主要用于限定容量(比如内存大小或缓存数量)的缓存,需要在缓存容器满了之后踢出过期缓存的策略,是使用次数最少或很久没使用的缓存项策略。
实现原理一般使用链表方式把所有缓存项连起来,每当有新的缓存进入则把缓存放入链表前端,如果缓存被使用则把他提到链表前端,那么没被使用的将慢慢趋于链表后端,所以当容量满了以后,就优先移除链表末尾的缓存项。当然,也有其他更为复杂的过期策略,比如同时使用缓存时间。虽然此策略和上面的按时间间隔延长缓存有点相像,但这个更侧重于缓存容器大小的管理,毕竟内存是有限的,此策略多用于公共缓存服务。下面的类是个简单的LRU实现,只限定的缓存的长度并没有大小限制,如果要做大小限制则需要计算每一个value的大小。
view
plain
/// <summary>
/// LRUCache
/// </summary>
public class LRUCache<TKey,TValue>
{
/// <summary>
/// 缓存项
/// </summary>
class CacheItem
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public CacheItem Left { get; set; }
public CacheItem Right { get; set; }
public CacheItem(TKey key, TValue value)
{
Key = key;
Value = value;
}
}
private readonly static object LockDict = new object();
private readonly IDictionary<TKey, CacheItem> _dict;
public int Length { get; private set; }
public LRUCache(int maxLength)
{
_dict = new Dictionary<TKey, CacheItem>();
Length = maxLength;
}
//链表头部
private CacheItem _first;
//链表末端
private CacheItem _last;
public bool HasKey(TKey key)
{
return _dict.ContainsKey(key);
}
/// <summary>
/// 添加一个缓存项
/// </summary>
public void Add(TKey key, TValue value)
{
var item = new CacheItem(key, value);
lock (LockDict)
{
//如果没有缓存项,则item既是first也是last
if (_dict.Count == 0)
{
_last = _first = item;
}
//如果只有一个缓存项,则item是first,first和last变为last
else if (_dict.Count == 1)
{
_last = _first;
_first = item;
_last.Left = _first;
_first.Right = _last;
}
else
{
//item为first,之前的前端向后移位
item.Right = _first;
_first.Left = item;
_first = item;
}
//如果超过的链表长度
if (_dict.Count >= Length)
{
//断开last并移除
_last.Left.Right = null;
_dict.Remove(_last.Key);
_last = _last.Left;
}
//将item放入dict
if (_dict.ContainsKey(key))
_dict[key] = new CacheItem(key, value);
else
_dict.Add(key, new CacheItem(key, value));
}
}
/// <summary>
/// 获取一个缓存项
/// </summary>
public TValue Get(TKey key)
{
if (!_dict.ContainsKey(key))
{
return default(TValue);
}
var item = _dict[key];
lock (LockDict)
{
if (_dict.Count == 1)
{
return item.Value;
}
//如果item左侧有缓存项,则将左侧的缓存指向item的右侧
if (item.Left != null)
{
item.Left.Right = item.Right;
}
else
{
//否则说明item是first
return item.Value;
}
//如果item右侧有缓存项,则将右侧的缓存指向item的左侧
if (item.Right != null)
{
item.Right.Left = item.Left;
}
else
{
//否则说明item是last
//将last的左侧的右侧断开,让其成为last
_last.Left.Right = null;
_last = _last.Left;
}
//断开item的左侧,让item成为first,让first成为item的右侧项
item.Left = null;
item.Right = _first;
_first.Left = item;
_first = item;
}
return item.Value;
}
public void Remove(TKey key)
{
if (!_dict.ContainsKey(key))
{
return;
}
var item = _dict[key];
lock (LockDict)
{
//如果item左侧有值,则将左侧的右侧指向item的右侧
if (item.Left != null)
{
item.Left.Right = item.Right;
}
else
{
//否则item则是first,所以将item的右侧赋值给first
_first = item.Right;
}
//如果item的右侧有值,则将item的右侧的左值指向item的左侧
if (item.Right != null)
{
item.Right.Left = item.Left;
}
else
{
_last = item.Left;
}
_dict.Remove(key);
}
}
}
以上提到的是我们常用的几种缓存策略,当然还有其他的策略,我们后面也会提到。今天就先到这吧。
相关文章推荐
- 带你走进缓存世界(1):漫谈缓存
- 带你走进缓存世界(2):缓存入门
- 一起谈.NET技术,带你走进缓存世界
- 带你走进缓存世界(6):共享缓存
- 带你走进缓存世界(2):缓存入门
- 带你走进缓存世界(3):缓存原理
- 带你走进缓存世界(6):共享缓存
- 带你走进缓存世界
- 带你走进缓存世界(3):缓存原理
- “.NET研究”带你走进缓存世界
- 带你走进缓存世界(4):缓存之缓
- 带你走进缓存世界(5):一显身手
- 带你走进缓存世界(3):缓存原理
- 带你走进缓存世界(5):一显身手
- 走进缓存的世界(三) - Memcache
- 带你走进缓存世界(1):漫谈缓存
- 走进缓存的世界(二) - 缓存设计
- 走进缓存的世界(一) - 开篇
- 带你走进缓存世界
- 【转】带你走进缓存的世界