惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Security Latest
Security Latest
P
Palo Alto Networks Blog
AWS News Blog
AWS News Blog
NISL@THU
NISL@THU
T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Latest news
Latest news
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
J
Java Code Geeks
P
Privacy International News Feed
阮一峰的网络日志
阮一峰的网络日志
S
Schneier on Security
博客园 - 聂微东
Project Zero
Project Zero
美团技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Scott Helme
Scott Helme
I
Intezer
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
S
Security @ Cisco Blogs
博客园 - 司徒正美
O
OpenAI News
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
酷 壳 – CoolShell
酷 壳 – CoolShell
SecWiki News
SecWiki News
月光博客
月光博客
S
Security Affairs
The GitHub Blog
The GitHub Blog
P
Privacy & Cybersecurity Law Blog
S
Secure Thoughts
V
V2EX
S
Securelist
F
Fortinet All Blogs
W
WeLiveSecurity
D
Docker
博客园 - 三生石上(FineUI控件)
Simon Willison's Weblog
Simon Willison's Weblog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Visual Studio Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Webroot Blog
Webroot Blog
Engineering at Meta
Engineering at Meta

博客园 - 一味

[翻译]实例:在Android调用WCF服务 不是架构的架构之五:业务层的实现与自动代理(补充) 不是架构的架构之四:业务层的实现与自动代理 不是架构的架构之三:系统基础(2)主键选择和并发 不是架构的架构之二:系统基础(1) 不是架构的架构之一:总体思路 一道算法题 技术先行or业务先行 用了一年的键盘,记录下键盘磨损的状况 一个业务系统设计构想(一) 安装VS2008/.Net3.5/.Net3.0/.Net2.0sp1失败的解决办法 常用存储过程4(K310。3版本获取物流新单据的编码) 常用存储过程3(获取编码的上级编码和短编码) 常用存储过程2(获取编码级次) 常用存储过程1(获取字符串中的第一个数值) SQL Server中Rollup关键字使用技巧 Math.Round函数四舍五入的问题 SQL Server进程阻塞的检查和解决办法(转自好友Blog) 学习NHibernate的感悟和疑惑
基于LRU淘汰的高性能缓存
一味 · 2011-07-15 · via 博客园 - 一味

  对于一个对性能要求较高的应用程序,使用缓存似乎是必然的选择。分布式系统通常选择分布式的缓存组件,如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核心数量决定。虽然不是真正的线程,但是实际上模拟了大并发下的真实场景。