基于LRU淘汰的高性能缓存

  对于一个对性能要求较高的应用程序,使用缓存似乎是必然的选择。分布式系统通常选择分布式的缓存组件,如memCache。而对于小型系统而言,memCache太沉重了,另外序列化的损失也让我放弃它重新实现InProc的数据缓存。

  LRU(Least Recent Used)是一种常用的缓存淘汰方式,在这里通过一个双向链表实现。

public class SimpleLRU<T> : ILRU<T>
{
private readonly LinkedList<T> _linklist = new LinkedList<T>();
private readonly int _maxitem;
private readonly int _removeRate;
/// <summary>
/// 在淘汰时和过期时通知缓存字典移除缓存
/// </summary>
private readonly Action<LinkedListNode<T>> onRemoveNode;

public SimpleLRU(int maxitem, int removeRate, Action<LinkedListNode<T>> onRemoveNode)
{
_maxitem
= maxitem;
_removeRate
= removeRate;
this.onRemoveNode = onRemoveNode;
}
/// <summary>
/// 移除一个缓存项,通常在显示标记缓存过期时使用
/// </summary>
public void Remove(LinkedListNode<T> node)
{
_linklist.Remove(node);
onRemoveNode(node);
}
/// <summary>
/// 标记缓存刚使用过
/// </summary>
public void MarkUse(LinkedListNode<T> node)
{
if(node.List!=null)
_linklist.Remove(node);
_linklist.AddFirst(node);
}
/// <summary>
/// 新增一个缓存项
/// </summary>
public LinkedListNode<T> AddNew(T value)
{
var n
= _linklist.AddFirst(value);
AutoKnockOut();
return n;
}
/// <summary>
/// 当容量超出最大容量时,按比例淘汰一部分
/// </summary>
public void AutoKnockOut()
{
if (_linklist.Count > _maxitem)
RemoveFromEnd(_maxitem
* _removeRate / 100);
}
private void RemoveFromEnd(int n)
{
if (_linklist.Count < n)
{
return;
}
for (; n > 0; n--)
{
Remove(_linklist.Last);
}
}
/// <summary>
/// 当前缓存大小
/// </summary>
public int Count
{
get { return _linklist.Count; }
}
}

  缓存容器的代码实现:

public class Cache<TKey, TValue>
{
private class CacheItem
{
public LinkedListNode<TKey> node;
public TValue value;
}
private readonly Dictionary<TKey, CacheItem> _dic;
private readonly ILRU<TKey> _linklist;

public Cache(int maxItems, int removeRatio)
{
_dic
= new Dictionary<TKey, CacheItem>(1000);
_linklist
= new SimpleLRU<TKey>(maxItems, removeRatio, OnRemoveNode);

}

/// <summary>
/// 从缓冲中淘汰
/// </summary>
void OnRemoveNode(LinkedListNode<TKey> node)
{
_dic.Remove(node.Value);
}
/// <summary>
/// 标记缓存过期
/// </summary>
public void Remove(TKey key)
{
lock (this)
{
CacheItem item;
if (_dic.TryGetValue(key, out item))
{
_linklist.Remove(item.node);
}
}
}
/// <summary>
/// 设置一个缓存,如果已经存在则更新
/// </summary>
public void Set(TKey key, TValue value)
{
CacheItem item;
lock (this)
{
if (_dic.TryGetValue(key, out item))
{
item.value
= value;
_linklist.MarkUse(item.node);
}
else
{
item
= new CacheItem { node = _linklist.AddNew(key), value = value };
_dic.Add(key, item);
}
}
}
/// <summary>
/// 获取一个缓存项
/// </summary>
public bool Get(TKey key, out TValue value)
{
CacheItem item;
lock (this)
{
if (_dic.TryGetValue(key, out item))
{
_linklist.MarkUse(item.node);
value
= item.value;
return true;
}
}
value
= default(TValue);
return false;

}
public int Count
{
get
{
return _linklist.Count;
}
}
}

  由于缓存可能存在并发,线程同步是最难以处理的事情,这里用了同步锁,强行将请求串行化来回避并发带来的问题,当然性能不是最佳的。

-------------------------------------------------------------------------------

  性能测试:

static void Main(string[] args)
{
	//测试无需淘汰的情况
	TestCache(1,1000000,1000000);
	TestCache(5, 1000000, 5000000);
	TestCache(10, 100000, 1000000);
	TestCache(50, 100000, 5000000);

	//需要淘汰的情况
	TestCache(1, 1000000, 80000);
	TestCache(5, 1000000, 80000);
	TestCache(10, 100000, 80000);
	TestCache(50, 100000, 80000);

	Console.ReadLine();
}
public static void TestCache(int threadCount, int count, int maxitem)
{
	Console.WriteLine("线程数:{0}\t循环次数:{1}\t缓存容量:{2}", threadCount, count, maxitem);
	const int rate = 8;

	var cache = new Cache<int, int>(maxitem, rate);

	var timer = new long[threadCount];
	var timer2 = new long[threadCount];

	Parallel.For(0, threadCount, t =>
									{
										var time = t + 1;
										var sp = new Stopwatch();
										sp.Start();

										for (var i = 0; i < count; i++)
										{
											cache.Set(i * time, i * time);
										}

										sp.Stop();
										timer[t] = sp.ElapsedMilliseconds;

										sp.Reset();
										sp.Start();

										for (var i = 0; i < count; i++)
										{
											int j = 0;
											if (cache.Get(i * time, out j))
											{
												Debug.Assert(j == i * time);
											}
										}

										sp.Stop();
										timer2[t] = sp.ElapsedMilliseconds;
									});


	Console.WriteLine("当前缓存大小:{0}", cache.Count);
	Console.WriteLine("{0}个线程分别写入缓存{2}次执行总时间:{1}", timer.Length, timer.Sum() / 1000.0, count);
	Console.WriteLine("{0}个线程分别读取缓存{2}次执行总时间:{1}", timer2.Length, timer2.Sum() / 1000.0, count);
	Console.WriteLine("---------------------------");
}

测试机器:

CPU:AMD Athlon*2 4800+ (2.5GHz)  Memory:2G

测试结果正如先前估计的一样,单线程下表现优异,在家里的破机器上读取速度接近千万/秒,写速度也达到百万/秒。而在多线程的环境下,性能大幅降低,并发越多性能下降越厉害,不过50个线程并发读写同一个缓存可能已经是极限情况,这时读取速度大约20万/秒,写速度10万/秒,对于多数应用,应该也可以接受。

如果你有更好的实现办法,不妨留言或者邮件告诉我,感激不尽。

补充:测试代码中使用的“线程”并非真正的线程(Thread),而是并行库中的任务,实际的线程可能由并行库根据机器的CPU核心数量决定。虽然不是真正的线程,但是实际上模拟了大并发下的真实场景。


posted @ 2011-07-15 09:57  一味  阅读(3516)  评论(14编辑  收藏  举报