蛙蛙推荐: LRU缓存的实现算法讨论

业务模型

读、写、删的比例大致是7:3:1,至少要支持500w条缓存,平均每条缓存6k,要求设计一套性能比较好的缓存算法。
算法分析

不考虑MemCachedVelocity等现成的key-value缓存方案,也不考虑脱离.net gc自己管理内存,不考虑随机读取数据及顺序读取数据的场景,目前想到的有如下几种LRU方案

算法

分析

SortedDictionary

.net自带的,内部用二叉搜索树(应该不是普通树,至少是做过优化的树)实现的,检索为O(log n),比普通的DictionayO(1))慢一点。
插入和删除都是O(log n),而且插入和删除,会实时排序。
但是.net 2.0的这个类没有First属性

Dictionary + PriorityQueue

Dictionay可以保证检索是O(1)
优先队列可以保证插入和删除都为O(log n)
但是优先队列删除指定的项不支持(至少我找到的优先队列不支持),所以在删除缓存的时候不知道咋实现

Dictionay + Binary heap

二叉堆也是优先队列,分析应该同上,我没有详细评估。

b

查找,删除,插入效率都很好,数据库都用它,但实现复杂,写一个没有BUGB树几乎不可能。有人提到stl:map是自顶向下的红黑树,查找,删除,插入都是O(log n),但咱不懂c++,没做详细测试。

Dictionay + List

Dict用来检索;
List用来排序;
检索、添加、删除都没问题,只有在清空的时候需要执行List的排序方法,这时候缓存条目比较多的话,可能比较慢。

Dictionay + LinkedList

Dict用来检索;
LinkedList的添加和删除都是O(1),添加缓存时在链表头加节点,获取缓存时把特定节点移动(先删除特定节点(O(n)),再到头部添加节点(O(1)))到头,缓存满地时候截断掉尾部的一些节点。

目前几种方案在多线程下应该都需要加锁,不太好设计无锁的方案,下面这个链接是一个支持多线程的方案,但原理至今没搞特明白
A High Performance Multi-Threaded LRU Cache
http://www.codeproject.com/KB/recipes/LRUCache.aspx

用普通链表简单实现LRU缓存
以下是最后一种方案的简单实现,大家讨论下这个方案值不值得优化,或者其它的哪个方案比较合适
 

public class LRUCacheHelper<K, V> {
    
readonly Dictionary<K, V> _dict;
    
readonly LinkedList<K> _queue = new LinkedList<K>();
    
readonly object _syncRoot = new object();
    
private readonly int _max;
    
public LRUCacheHelper(int capacity, int max) {
        _dict 
= new Dictionary<K, V>(capacity);
        _max 
= max;
    }
 
    
public void Add(K key, V value) {
        
lock (_syncRoot) {
            checkAndTruncate();
            _queue.AddFirst(key);   
//O(1)
            _dict[key] = value;     //O(1)
        }
    }
 
    
private void checkAndTruncate() {
        
lock (_syncRoot) {
            
int count = _dict.Count;                        //O(1)
            if (count >= _max) {
                
int needRemoveCount = count / 10;
                
for (int i = 0; i < needRemoveCount; i++) {
                    _dict.Remove(_queue.Last.Value);        
//O(1)
                    _queue.RemoveLast();                    //O(1)
                }
            }
        }
    }
 
    
public void Delete(K key) {
        
lock (_syncRoot) {
            _dict.Remove(key); 
//(1)
            _queue.Remove(key); // O(n)
        }
    }
    
public V Get(K key) {
        
lock (_syncRoot) {
            V ret;
            _dict.TryGetValue(key, 
out ret);    //O(1)
            _queue.Remove(key);                 //O(n)
            _queue.AddFirst(key);               //(1)
            return ret;
        }
    }
}


用双头链表代替普通链表

突然想起来了,可以把链表换成双头链表,然后在字典里保存链表节点,在Get方法的时候直接从字典里获取到要移动的节点,然后把这个节点的上一个节点的Next指针指向给下一个节点,下一个节点的Previous指针指向上一个节点,这样就把移动节点的操作简化成O(1)了,提高了缓存读取的效率。

_dict.TryGetValue(key, out ret);    //O(1)
ret.Next.Previous = ret.Previous     //O(1)
ret. Previous.Next. = ret.Next         //O(1)
  _queue.AddFirst(key);                      //O(1)

我改进后的链表就差不多满足需求了,

操作

基本操作

复杂度

读取

Dict.Get

Queue.Move

O 1

O 1

删除

Dict.Remove

Queue.Remove

O 1

O 1

增加

Dict.Add

Queue.AddFirst

O 1

O 1

截断

Dict.Remove

Queue.RemoveLast

O k

O k

K表示截断缓存元素的个数

 

其中截断的时候可以指定当缓存满的时候截断百分之多少的最少使用的缓存项。

其它的就是多线程的时候锁再看看怎么优化,字典有线程安全的版本,就把.net 3.0的读写锁扣出来再把普通的泛型字典保证成ThreadSafelyDictionay就行了,性能应该挺好的。

链表的话不太好用读写锁来做线程同步,大不了用互斥锁,但得考虑下锁的粒度,Move,AddFirst,RemoveLast的时候只操作一两个节点,是不是想办法只lock这几个节点就行了,Truncate的时候因为要批量操作很多节点,所以要上个大多链表锁,但这时候怎么让其它操作停止得考虑考虑,类似数据库的表锁和行锁。

实现代码

 

public class DoubleLinkedListNode<T> {
    
public T Value { getset; }
 
    
public DoubleLinkedListNode<T> Next { getset; }
 
    
public DoubleLinkedListNode<T> Prior { getset; }
 
    
public DoubleLinkedListNode(T t) { Value = t; }
 
    
public DoubleLinkedListNode() { }
 
    
public void RemoveSelf() {
        Prior.Next 
= Next;
        Next.Prior 
= Prior;
    }
 
}
public class DoubleLinkedList<T> {
    
protected DoubleLinkedListNode<T> m_Head;
    
private DoubleLinkedListNode<T> m_Tail;
 
    
public DoubleLinkedList() {
        m_Head 
= new DoubleLinkedListNode<T>();
        m_Tail 
= m_Head;
    }
 
    
public DoubleLinkedList(T t)
        : 
this() {
        m_Head.Next 
= new DoubleLinkedListNode<T>(t);
        m_Tail 
= m_Head.Next;
        m_Tail.Prior 
= m_Head;
    }
 
    
public DoubleLinkedListNode<T> Tail {
        
get { return m_Tail; }
    }
 
    
public DoubleLinkedListNode<T> AddHead(T t) {
        DoubleLinkedListNode
<T> insertNode = new DoubleLinkedListNode<T>(t);
        DoubleLinkedListNode
<T> currentNode = m_Head;
        insertNode.Prior 
= null;
        insertNode.Next 
= currentNode;
        currentNode.Prior 
= insertNode;
        m_Head 
= insertNode;
        
return insertNode;
    }
    
public void RemoveTail() {
        m_Tail 
= m_Tail.Prior;
        m_Tail.Next 
= null;
        
return;
    }
}
public class LRUCacheHelper<K, V> {
    
class DictItem {
        
public DoubleLinkedListNode<K> Node { getset; }
        
public V Value { getset; }
    }
    
readonly Dictionary<K, DictItem> _dict;
    
readonly DoubleLinkedList<K> _queue = new DoubleLinkedList<K>();
    
readonly object _syncRoot = new object();
    
private readonly int _max;
    
public LRUCacheHelper(int capacity, int max) {
        _dict 
= new Dictionary<K, DictItem>(capacity);
        _max 
= max;
    }
 
    
public void Add(K key, V value) {
        
lock (this)
        {
 
            checkAndTruncate();
            DoubleLinkedListNode
<K> v = _queue.AddHead(key);   //O(1)
            _dict[key] = new DictItem() { Node = v, Value = value }; //O(1)
        }
    }
 
    
private void checkAndTruncate() {
        
int count = _dict.Count;                        //O(1)
        if (count >= _max) {
            
int needRemoveCount = count / 10;
            
for (int i = 0; i < needRemoveCount; i++) {
                _dict.Remove(_queue.Tail.Value);        
//O(1)
                _queue.RemoveTail();                    //O(1)
            }
        }
    }
 
    
public void Delete(K key) {
        
lock (this) {
            _dict[key].Node.RemoveSelf();
            _dict.Remove(key); 
//(1) 
        }
    }
    
public V Get(K key) {
        
lock (this) {
            DictItem ret;
            
if (_dict.TryGetValue(key, out ret)) {
                ret.Node.RemoveSelf();
                _queue.AddHead(key);
                
return ret.Value;
            }
            
return default(V); 
        }
    }
}


性能测试

用双头链表测试了一下,感觉性能还可以接受,每秒钟读取可达80w,每秒钟写操作越20w

程序初始化200w条缓存,然后不断的加,每加到500w,截断掉10分之一,然后继续加。

测试模型中每秒钟的读和写的比例是7:3,以下是依次在3个时间点截取的性能计数器图。
图1

 
图2

 

 
图3

 


内存最高会达到
1gcpu也平均百分之90以上,但测试到后期会发现每隔一段时间,就会有一两秒,吞吐量为0,如最后一张截图,后来观察发现,停顿的那一两秒是二代内存在回收,等不停顿的时候# gen 2 collections就会加1,这个原因应该是链表引起的,对链表中节点的添加和删除是很耗费GC的,因为会频繁的创建和销毁对象。 

后续改进

1、 用游标链表来代替普通的双头链表,程序起来就收工分配固定大小的数组,然后用数组的索引来做链表,省得每次添加和删除节点都要GC的参与,这相当于手工管理内存了,但目前我还没找到c#合适的实现。

2、 有人说链表不适合用在多线程环境中,因为对链表的每个操作都要加互斥锁,连读写锁都用不上,我目前的实现是直接用互斥锁做的线程同步,每秒的吞吐量七八十万,感觉lock也不是瓶颈,如果要改进的话可以把DictionaryThreadSafelyDictionary来代替,然后链表还用互斥锁(刚开始设想的链表操作只锁要操作的几个节点以降低并发冲突的想法应该不可取,不严谨)。

3、 还有一个地方就是把锁细分以下,链表还用链表,但每个链表的节点是个HashSet,对HashSet的操作如果只有读,写,删,没有遍历的话应该不需要做线程同步(我感觉不用,因为Set就是一个集合,一个线程往里插入,一个线程往里删除,一个线程读取应该没问题,顶多读进来的数据可能马上就删除了,而整个Set的结构不会破坏)。然后新增数据的时候往链表头顶Set里插入,读取某个数据的时候把它所在的节点的Set里删除该数据,然后再链表头的Set里插入一个数据,这样反复操作后,链表的最后一个节点的Set里的数据都是旧数据了,可以安全的删除了,当然这个删除的时候应该是要锁整个链表的。每个Set应该有个大小上限,比如20w,但set不能安全的遍历,就不能得到当前大小,所以添加、删除Set的数据的时候应该用Interlocked.Decrement() Interlocked.Increment()维护一个Count,一遍一个Set满的时候,再到链表的头新增一个Set节点。

性能测试脚本

class Program {
    
private static PerformanceCounter _addCounter;
    
private static PerformanceCounter _getCounter;
 
    
static void Main(string[] args) {
        SetupCategory();
        _addCounter 
= new PerformanceCounter("wawasoft.lrucache""add/sec"false);
        _getCounter 
= new PerformanceCounter("wawasoft.lrucache""get/sec"false);
        _addCounter.RawValue 
= 0;
        _getCounter.RawValue 
= 0;
 
        Random rnd 
= new Random();
        
const int max = 500 * 10000;
 
 
        LRUCacheHelper
<intint> cache = new LRUCacheHelper<intint>(200 * 10000, max);
 
        
for (int i = 10000*100000 - 1; i >= 0; i--)
        {
            
if(i % 10 > 7)
            {
                ThreadPool.QueueUserWorkItem(
                    
delegate
                        {
                            cache.Add(rnd.Next(
010000), 0);
                            _addCounter.Increment(); 
                        });
            }
            
else
            {
                ThreadPool.QueueUserWorkItem(
                   
delegate
                   {
                       
int pop = cache.Get(i);
                       _getCounter.Increment();
                   });
            }
        }
        Console.ReadKey();
    }
 
    
private static void SetupCategory() {
        
if (!PerformanceCounterCategory.Exists("wawasoft.lrucache")) {
 
            CounterCreationDataCollection CCDC 
= new CounterCreationDataCollection();
 
            CounterCreationData addcounter 
= new CounterCreationData();
            addcounter.CounterType 
= PerformanceCounterType.RateOfCountsPerSecond32;
            addcounter.CounterName 
= "add/sec";
            CCDC.Add(addcounter);
 
 
            CounterCreationData getcounter 
= new CounterCreationData();
            getcounter.CounterType 
= PerformanceCounterType.RateOfCountsPerSecond32;
            getcounter.CounterName 
= "get/sec";
            CCDC.Add(getcounter);
 
            PerformanceCounterCategory.Create(
"wawasoft.lrucache","lrucache",CCDC);
 
        }
    }
 
}


参考链接

不分先后,都是随时从网上找的,大多看不懂
潜心学习数据结构-C#语言描述系列文章
http://space.cnblogs.com/group/topic/6922/
《C++数据结构原理与经典问题求解》
http://www.huachu.com.cn/itbook/bookinfodetail.asp?lbbh=10087298&sort=ml
数据结构(C#):循环链表
http://www.cnblogs.com/zxjay/archive/2008/12/07/1349688.html
表的游标实现
http://www.comp.nus.edu.sg/~xujia/mirror/algorithm.myrice.com/datastructure/basic/list/chapter3_3.htm
我所理解的链表1
http://www.diybl.com/course/3_program/c/csuanfa/2007213/21570.html
cursor implementation of linked list
http://wiki.answers.com/Q/Explain_the_Cursor_implementation_of_linked_list
Sorting Algorithms In C#
http://www.codeproject.com/KB/recipes/cssorters.aspx
The C5 Generic Collection Library
http://www.itu.dk/research/c5/
当弱引用对象成为集合元素时
http://www.agiledon.com/post/Coding/Weakreference-Collection-CSharp.html
QuickSort in Functional C#
http://blogs.objectsharp.com/cs/blogs/jlee/archive/2008/05/23/quicksort-in-functional-c.aspx
LRU页面置换算法模拟
http://dev.csdn.net/article/73207.shtm

posted @ 2009-07-22 22:59 蛙蛙王子 Views(3447) Comments(44) Edit 收藏

 回复 引用   
#1楼2009-07-22 23:08 | 方法的[未注册用户]
蛙蛙是个好娃娃
 回复 引用 查看   
#2楼2009-07-22 23:23 | Steven Chen      
我是来顶贴并收藏的
 回复 引用 查看   
#3楼2009-07-22 23:37 | shawnliu      
什么cache这么大
5000000×6k 大概 30G 吧
大哥你什么内存啊 需求考虑周全了吗

 回复 引用 查看   
#4楼[楼主]2009-07-22 23:48 | 蛙蛙池塘      
@shawnliu
说多了,呵呵,500w*6k是总共的缓存,需要多台缓存服务器来做,就当是200w吧,一个64位机器用6g内存还是可以的吧,实在不行,每个缓存1k,6k有点儿过了,6k是个很大的数字了,刚开始估算错了我。

 回复 引用 查看   
#5楼2009-07-22 23:48 | shawnliu      
你那个性能测试脚本问题也多

if(i % 10 > 7)这里余数只会是9,8,显然读写比是8:2
你key生成是10000内随机数 这似的dictionary的冲突大很多 绝对要再hash 性能差很多 value全部是0,怎么不是个6k左右数据呢

 回复 引用 查看   
#6楼2009-07-22 23:54 | shawnliu      
这种规模显然要考虑分布式吧 不然你这台cache机器挂了 可靠性就很差 还是考虑memcached这样的来满足需求吧 毕竟已经有很多成功的案例 上几百台机器甚至上千台机器什么cache搞不定 非托管代码这时候优势明显 你上面那个读写比例使得cache命中率很差的
 回复 引用 查看   
#7楼2009-07-22 23:58 | shawnliu      
上面测试的速度也基本不可靠 太多因素没考虑到 网络 服务器负载 真实cache数据分布
 回复 引用 查看   
#8楼2009-07-23 00:01 | shawnliu      
这个案例倒是学习同步的很好材料 思考思考
 回复 引用 查看   
#9楼[楼主]2009-07-23 00:11 | 蛙蛙池塘      
@shawnliu
% 10 >7确实不读,应该>=7,随机数的范围也应该改改,都是之前测的,忘了改了,不过dictionary处理冲突很高效的,碰撞后用的是游标链表存储的碰撞数据,我本机就2g内存,而且测的主要是时间复杂度,没考虑value的大小,对测试影响不大。
我改改脚本再测测,你看到挺仔细的,呵呵。
至于测试速度不可靠,只是测的理想情况下,真实情况肯定要打折的。
分布式肯定要考虑的,直接用memocached也考虑过,发此贴主要讨论思路。

 回复 引用 查看   
#10楼2009-07-23 00:17 | Cheney Shue      
@shawnliu
不要这么较真,楼主就是简单的实现LRU的原理而已,没有任何实用性,就当是大学作业。
如果要性能,就用c了,什么Dictionary也不用,直接用机器码。要从intel的开发知识库下手。

 回复 引用 查看   
#11楼[楼主]2009-07-23 07:25 | 蛙蛙池塘      
@Cheney Shue
shawnliu也不是较真,提的意见挺好的,写代码就得严谨点儿,多考虑一些因素。
你说的几句我倒是不太同意,我写这个东西虽然是探讨思路,但最终的目的还是想用在实际应用中的,只要能满足业务的功能需求和性能需求,就有实用性,c#做好了照样用,c不是万能的,机器码应该用不到吧。
memocached也不是用c写的呀,velocity应该就是用c#写的吧,主要是不会c,更别提intel知识库了,呵呵。
c#把数据结构和算法确认下来了,找个会c的,改一下就行了,对吧。
大家对数据结构和并发支持上有哪些改进的建议,可以提提,咱们慢慢改进。

 回复 引用 查看   
#12楼2009-07-23 08:45 | 莫贝特(MBetter)      
学习
 回复 引用 查看   
#13楼2009-07-23 08:47 | Jeffrey Zhao      
在这里的性能方面的主要矛盾还是锁,至于动辄C/汇编,过了。
操作系统中也就线程调度等核心部分用了汇编,其余都是C,还有大量C++。
语言/平台造成的性能不是那么明显的,就算是矛盾,也是次要矛盾。

 回复 引用 查看   
#14楼2009-07-23 08:52 | Jeffrey Zhao      
引用shawnliu:这种规模显然要考虑分布式吧 不然你这台cache机器挂了 可靠性就很差 还是考虑memcached这样的来满足需求吧 毕竟已经有很多成功的案例 上几百台机器甚至上千台机器什么cache搞不定 非托管代码这时候优势明显 你上面那个读写比例使得cache命中率很差的

memcached号称是“分布式缓存”,其实没有分布式的,是单点缓存。
一台memcached挂了,这个也就挂了。

 回复 引用 查看   
#15楼2009-07-23 09:04 | Galactica      
楼主用了几条线程做测试?
我还是希望楼主用VS Test来模拟并发用户测试;或者自己写;请不要用ThreadPool.QueueUserWorkItem这样的语句来模拟并发和测试系统负载。

你实际的并发数不超过20。

PerformanceCounter最好加到你的LRUCacheHelper<U,V>.Add 里面。


 回复 引用 查看   
#16楼2009-07-23 09:17 | wuxiaoqqqq      
都忘光了,还是去GOOGLE了一下才知道LRU是什么。
 回复 引用 查看   
#17楼[楼主]2009-07-23 09:30 | 蛙蛙池塘      
@Galactica
计数器为啥要加到cachehelper里呀,匿名委托会自动记住临时变量的呀,用threadpool模拟的就是多并发吧,和vs test不一样吗?你的意思是真实情况下,锁争用应该更大,对吧。
指点一下。

 回复 引用 查看   
#18楼2009-07-23 09:43 | Galactica      
CodeProject上的那篇文章使用了WeakReference,基本上.net下的资源池都是用这个东西。

老外使用LinkedList存储实际的object,然后使用Dictionary<Key,Value>存储index和object WeakReference,
这基本上就是ADO.NET中连接池的实现方式。同时,老外又采用Patterns&Practices的The Caching Application Block 中管理过期缓存的策略,使用创建副本的模式来过渡缓存过期更新。

WeakReference很好的利用GC来保证Least recently used策略,同时又减少了对象创建次数。

 回复 引用 查看   
#19楼[楼主]2009-07-23 09:47 | 蛙蛙池塘      
@Galactica
明白一些了,谢谢,弱引用在缓存里肯定要用的,我当时没想到怎么用。
给详细解释下The Caching Application Block 吧,只有LRU还是还有缓存依赖等功能呀。

 回复 引用 查看   
#20楼2009-07-23 09:50 | shawnliu      
@Jeffrey Zhao
你说的对。相对可靠性高一些。如果想更高你就要多存几个备份。
这个可以动手脚的地方很多,要做到也不难吧,比如具体hash过去的那个节点是虚的 后面挂几个实的 这样可靠性提高 或者设计来区分cache丢失的严重性,不严重就一份,严重就自己考虑放几份吧。

 回复 引用 查看   
#21楼2009-07-23 09:54 | shawnliu      
@蛙蛙池塘
你确定dictionary碰撞以后是挂链表的吗? 貌似不是吧 java里面是这么干的 .net好象是再hash的吧

 回复 引用 查看   
#22楼2009-07-23 09:54 | BillGan      
@Jeffrey Zhao
memcached号称是“分布式缓存”,其实没有分布式的,是单点缓存。
一台memcached挂了,这个也就挂了。
-------------------------------------------------------------
一台挂了,也不会引起所有都挂吧,只是其他机器因为哈希函数会引起部分缓存项丢失,但是这个丢失率好像不大,memcached 配置是一致哈希算法的时候。

 回复 引用 查看   
#23楼2009-07-23 10:12 | Galactica      
引用蛙蛙池塘:
@Galactica
计数器为啥要加到cachehelper里呀,匿名委托会自动记住临时变量的呀,用threadpool模拟的就是多并发吧,和vs test不一样吗?你的意思是真实情况下,锁争用应该更大,对吧。
指点一下。


计数器本身作为你设计的组件的一个重要组成部分,可以用来考量性能和运行环境下检测组件,所以把它放到组件内部。

在楼主perfmon截图中,Thread Count 21,说明当前ThreadPool中同时运行的线程数很少,也就是并发很少发生。

for(int i =0;i<100000;i++)
ThreadPool.QueueUserWorkItem

只是顺序把WorkItem放入ThreadPool,在你放入WorkItem的同时,存在ThreadPool中的WorkItem就已经开始工作了,并没有达到保证同时多个WorkItem工作的场景。

使用VS Test就是编写测试代码简单;自己编写测试组件可以使用WaitHandle达到更好的控制。

不过,楼主的代码稍作修改,也可以实现效果:

int round = 1000;
for(int i =0;i<round;i++)
{
cache.Add(rnd.Next(0, 10000), 0);
_addCounter.Increment();
}

 回复 引用 查看   
#24楼[楼主]2009-07-23 10:14 | 蛙蛙池塘      
@shawnliu
可靠性得单独设计和考虑,现在先不考虑,每插入一条缓存,可以同时插入到3台机器上,或者每台缓存服务器做硬盘备份。
.net字典里应该是用链表处理冲突的,没具体反编译看代码,听人说的。
@BillGan
老赵说到是这台挂了,这台就挂了,没有说都挂了。
一致性哈希是啥意思,就是同样的key都找同一个缓存服务器吧,
memcached有好多扩展,支持本地存储,冗余等特性。
看到一个帖子里说
“我们可以将memcached提升到每秒处理20万个UDP请求,平均延迟降低为173微秒。可以达到的总吞吐量为30万UDP 请求/s,”
http://shiningray.cn/scaling-memcached-at-facebook.html

 回复 引用 查看   
#25楼2009-07-23 10:23 | Galactica      
引用蛙蛙池塘:
@Galactica
明白一些了,谢谢,弱引用在缓存里肯定要用的,我当时没想到怎么用。
给详细解释下The Caching Application Block 吧,只有LRU还是还有缓存依赖等功能呀。


LRU,CacheDependency都有,还有其它的缓存过期策略,也可以自定义过期策略。
The Caching Application Block 我记得TerryLee应该介绍过,园子里也有很多文章,msdn上也有。楼主可以用这个东西也作下测试,我也可以顺便了解下它的性能。

使用链表的话,可以考虑C#下的无锁数据结构相关技术,这样可以更大限度的减少lock.

 回复 引用 查看   
#26楼[楼主]2009-07-23 10:43 | 蛙蛙池塘      
@Galactica 23楼
你最后给出的例子,是单线程的,就测试不出多线程的情况了吧,用线程池来测,如果每个Workitem执行足够快,也能模拟每秒很多请求的场景呀,我的计数器里也看到每秒有几十w的吞吐量呀,对吧。
可能你说的同时,不是说同一秒,不是并发,是并行,是这意思吗?

“自己编写测试组件可以使用WaitHandle达到更好的控制。”这句话没想明白,用waithandle还不如用threadpool呢吧。

@Galactica 25楼
今天晚上看俺看caching application block,链表应该没有锁无关的数据结构吧,而且无锁编程在强烈的并发情况下,性能不如互斥锁的。

 回复 引用 查看   
#27楼2009-07-23 10:47 | Cheney Shue      
楼上的几位说的对,实际应用中,可靠性和安全性很重要。内存这种东西要定期加电刷新,没有ECC的话,不安全。如果数据量大的话,寻址的效率就要考虑了,Dictionary尽管加了索引,但数据经常检索多个字段,不光是key。另外,如果是真实的缓存应用上,你还得在LRU的基础上改进算法,提高命中率。你是否还要设计个监控和管理模块……
看了你的博客,感觉你研究了很多,但都没有深入和实践过。你是否考虑做一个完整解决方案或是独立的产品,争取以后商业化。不要老是搞一些山寨的东西。

 回复 引用 查看   
#28楼2009-07-23 10:50 | eaglet      
为什么用了字典以后还要用一个链表来存储呢?楼主能给解释一下吗?

 回复 引用 查看   
#29楼[楼主]2009-07-23 10:50 | 蛙蛙池塘      
@Cheney Shue
谢了,你给出个方案吧,咱俩合伙搞,呵呵。

 回复 引用 查看   
#30楼[楼主]2009-07-23 10:52 | 蛙蛙池塘      
@eaglet
字典是哈希存储的,不能排序,就找不到最少被使用的数据,就不能删除过期数据。

 回复 引用 查看   
#31楼2009-07-23 11:09 | Cheney Shue      
引用蛙蛙池塘:
@Cheney Shue
谢了,你给出个方案吧,咱俩合伙搞,呵呵。


我搞实施的,平时不写代码,挺多就是做个网站赚点小钱,太复杂的东西搞不来。

 回复 引用 查看   
#32楼2009-07-23 11:09 | eaglet      
排序似乎用链表不是很好的解决方案。建议采用堆排序,即管理一个最少使用数据的堆。
而过期数据的处理可考虑采用分段存储,不需要排序。

 回复 引用 查看   
#33楼2009-07-23 11:11 | Galactica      
引用蛙蛙池塘:
@Galactica 23楼
你最后给出的例子,是单线程的,就测试不出多线程的情况了吧,用线程池来测,如果每个Workitem执行足够快,也能模拟每秒很多请求的场景呀,我的计数器里也看到每秒有几十w的吞吐量呀,对吧。
可能你说的同时,不是说同一秒,不是并发,是并行,是这意思吗?

“自己编写测试组件可以使用WaitHandle达到更好的控制。”这句话没想明白,用waithandle还不如用threadpool呢吧。

@Galactica 25楼
今天晚上看俺看caching application block,链表应该没有锁无关的数据结构吧,而且无锁编程在强烈的并发情况下,性能不如互斥锁的。

我再耐心的讲解一遍:
int round = 10000;
for (int i = 10000*100000 - 1; i >= 0; i--)
{
if(i % 10 > 7)
{
ThreadPool.QueueUserWorkItem(
delegate
{
for(int j =0;j<round;j++)
{
cache.Add(rnd.Next(0, 10000), 0);
_addCounter.Increment();
}
});
}
else
{
ThreadPool.QueueUserWorkItem(
delegate
{
for(int j = 0;j<round;j++)
{
int pop = cache.Get(i);
_getCounter.Increment();
}
});
}
}

因为你是顺序插入WorkItem,所以已经插入的WorkItem就可以执行了,这样就很难控制ThreadPool同时执行的WorkItem数目。一个简单方式就是延长每个WorkItem的执行时间,这样就可以让ThreadPool中同时工作的WorkItem数目增加。

使用WaitHandle的目的就是更精确的控制同时执行的WorkItem,同时,使用WaitHandle还可以采集更多的组件性能指标。

 回复 引用 查看   
#34楼[楼主]2009-07-23 11:17 | 蛙蛙池塘      
@Galactica
明白了,我在workitem里sleep几毫秒不就行啦,不过我就还是觉得那样测没必要,本来从缓存里取数据就是接近O 1的,为什么要增加延时呀,如果不考虑锁争用的话,读写应该非常非常快,只有处理哈希和内存寻址耗费的时间。
@eaglet
我了解下堆排序,关于分段存储,再说详细点儿给。
@Cheney Shue
做网站我也会,就是不知道做啥,呵呵

 回复 引用 查看   
#35楼2009-07-23 11:36 | Galactica      
引用蛙蛙池塘:
@Galactica
明白了,我在workitem里sleep几毫秒不就行啦,不过我就还是觉得那样测没必要,本来从缓存里取数据就是接近O 1的,为什么要增加延时呀,如果不考虑锁争用的话,读写应该非常非常快,只有处理哈希和内存寻址耗费的时间。
@eaglet
我了解下堆排序,关于分段存储,再说详细点儿给。
@Cheney Shue
做网站我也会,就是不知道做啥,呵呵


你还是不明白啊~,Sleep是没有用的,不是增加延时,而是增加Add,Get的并发数。你需要测试的是Add,Get随着并发数增加下的性能,而不是在调用之前就把并发数限制在20左右。因为实际的业务场景可能会超出你限定的20左右的并发数。

 回复 引用 查看   
#36楼2009-07-23 11:48 | eaglet      
分段存储就是按时间存放链表,比如每分钟一个链表,这个链表中的数据都是在这一分钟中缓存的。到时间后,只要把某个时间前的所有链表全部删除就完事了,这样做有点类似于基数排序,但对每个基数类面的数据不进行排序。这样做可以把200W数据分到几十个大的集合中间,每个集合对应一个时间段,集合内部不需要排序,那么你真正排序的就是那几十个集合,排序的时间几乎可以忽略不计。
另外你字典和链表之间现在是强连接,建议做成弱连接,即字典只对应一个索引号,这样删除数据只需要将对应链表设置为null就OK了,不需要将字典中所有对应元素都删除掉,标记已删除可以做在析构中。总之这里面还有很多技巧,需要开动脑筋。
另外这么大的内存,还是自己编写内存管理模块比较好,完全依靠GC,释放内存时恐怕会比较慢。

 回复 引用 查看   
#37楼[楼主]2009-07-23 13:00 | 蛙蛙池塘      
@eaglet
你提供的思路很好,和我改进建议里提到第3点比较像。
字典和链表做成弱链接,不懂怎么做,链表是不可以用索引访问的吧,我直接保存了一个链表的节点,然后通过字典的key能直接定位,可以删除自己,就一个引用而已,为什么叫强链接呀,链表得用游标实现后才能让字典保存链表节点的索引,c#里基于游标实现的链表我还没写出来呢。
啥叫不需要将字典所对应的元素都删除掉呀?dict.remove?析构函数是对象消亡的时候执行的,不remove掉,有人应用着它,怎么会执行析构函数呢。
自己做内存管理,应该一般人做不好吧,还得做内存页,内存块儿啥的吧。

@Galactica
不明白,就算有1w个客户端请求缓存服务,缓存服务也是先到线程池才能处理的,线程池就是20多个线程,挺好的呀,只有某个workitem执行很长时间,才会增加一个线程去处理新的请求,真实情况下应该也和这个差不多呀。

你说的并发数是啥意思呀,计数器里写着每秒几十w的并发请求呀,这不叫并发吗?1个线程1秒发1w个请求,并发请求是1还是1w呀。

 回复 引用 查看   
#38楼2009-07-23 14:05 | eaglet      
所以要用弱连接。链表从一个数组中申请,Dict 指向数组的索引,你那个强连接当然不能析构。其实这个弱连接的设计并非我讲的这么简单,里面还有很多技巧,我不可能在回复中详细说,篇幅不够。

实现内存自己管理是可以做的,不知道楼主这方面功底有多深。我过去做操作系统时,内存管理都是自己实现的,我们做的实时操作系统,用于上百万门的大型通信系统,每秒钟可以有几十万次并发,处理数十万用户同时呼叫。微软windows 2003的堆管理算法进行了重大改进,当时他们的工程师跑到我们公司来介绍算法,被我问了几个问题当场哑火。因为他们改进后的算法和我原来那个公司98年开发的内存管理算法原理上几乎是一样的,我对那套内存管理算法太熟悉了,算法的优缺点我都知道。而那个微软的哥们并不是总部那边的开发者,只是照本宣科,深入的东西他没有仔细考虑过。

 回复 引用 查看   
#39楼[楼主]2009-07-23 14:13 | 蛙蛙池塘      
@eaglet
哥们你太厉害了,俺写的不是Lru缓存,俺写的是寂寞。。。。。。

 回复 引用 查看   
#40楼2009-07-23 14:49 | Galactica      
引用蛙蛙池塘:
@eaglet
你提供的思路很好,和我改进建议里提到第3点比较像。
字典和链表做成弱链接,不懂怎么做,链表是不可以用索引访问的吧,我直接保存了一个链表的节点,然后通过字典的key能直接定位,可以删除自己,就一个引用而已,为什么叫强链接呀,链表得用游标实现后才能让字典保存链表节点的索引,c#里基于游标实现的链表我还没写出来呢。
啥叫不需要将字典所对应的元素都删除掉呀?dict.remove?析构函数是对象消亡的时候执行的,不remove掉,有人应用着它,怎么会执行析构函数呢。
自己做内存管理,应该一般人做不好吧,还得做内存页,内存块儿啥的吧。

@Galactica
不明白,就算有1w个客户端请求缓存服务,缓存服务也是先到线程池才能处理的,线程池就是20多个线程,挺好的呀,只有某个workitem执行很长时间,才会增加一个线程去处理新的请求,真实情况下应该也和这个差不多呀。

你说的并发数是啥意思呀,计数器里写着每秒几十w的并发请求呀,这不叫并发吗?1个线程1秒发1w个请求,并发请求是1还是1w呀。


不只是某个WorkItem执行很长时间,才会增加一个线程,而是业务处理有峰值,比如某个时候恰好100个请求同时访问缓存,每个业务系统都有自己的负载峰值和平均值,如果你对这些概念不懂,请先学习下。

20个线程的大小限制在大多数场景下都足够,但是现在随着硬件性能提升,20个线程越来越不够,所以微软才会把默认的线程池大小从22提高到250/cpu(核心数),一般业务场景的并发数估算是客户负载数目的5%-20%。

1个线程1w个请求,并发是1;如果按照每次Add 1毫秒,你的计数器会统计出1000次/每秒;如果每次Add 0.1毫秒,你的计数器会统计出 10000次/每秒;如果每次Add 0.01毫秒,你的计数器会统计出100000次/每秒.

你那个计数器的含义是每秒完成的请求数,不是并发数,请分清楚概念。在你的测试场景中,衡量并发数的标志是ThreadPool中同时执行的WorkItem个数,也就是Thread Count(这也是一个近似值,因为进程还有额外的线程需求,但是不会超过16个的误差)。

要不,我帮你测试下并发访问的性能?

 回复 引用 查看   
#41楼[楼主]2009-07-23 16:20 | 蛙蛙池塘      
@Galactica
明白了,你说是并行,不是并发吧,我本地应该怎么压也压不起来线程数吧,只要把workitem里sleep,线程数肯定会增加的,线程池是不用手工管理的,这么多请求,他就认为开20个请求就能处理过来。
如果同时有1000个请求过来,就一个线程,那肯定会排队呀,严格的并行处理应该是几个CPU能处理几个并行的请求吧。
我不是做性能测试的,对一些术语只是自己的理解,要不你给个链接我看下。
我理解的每秒处理多少个请求就代表吞吐量,就表示系统的负载,至于某个时间点同时在处理多少个请求,我不关心。

 回复 引用 查看   
#42楼[楼主]2009-07-23 16:23 | 蛙蛙池塘      
“并行”是指无论从微观还是宏观,二者都是一起执行的,就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。
而“并发”在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,从宏观外来看,好像是这些进程都在执行,这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
从以上本质不难看出,“并发”执行,在多个进程存在资源冲突时,并没有从根本提高执行效率。

http://zhidao.baidu.com/question/4923389.html

我还真不理解为什么微软要让一个CPU跑250个线程,如果一个CPU有250个线程,每个线程都在执行的话,那上下文切换应该很厉害吧,不如就两个线程处理,其余的排队呢。

 回复 引用 查看   
#43楼2009-07-23 18:14 | 徐少侠      
缓存的研究很有必要的

谁让现在的一些ORM方案里的缓存都不怎么让人舒服

尤其是在最大限度降低数据库读写这块

不过今天一个朋友的思路是这样的:缓存里的东西尽量是只读的,修改和删除操作直接跑数据库。想想也有点道理。
有时候我设计的数据库在日常工作中更新和删除操作的比例是很低的。

 回复 引用 查看   
#44楼2009-08-12 09:53 | overred      
测试脚本有问题:
ThreadPool.QueueUserWorkItem
非IO式的APM严重浪费你的性能消耗。(and rnd.Next)

直接裸体测试,get/sec达到7位数应该很轻松,CPU应该也不高

^_^
读取命中率,CPU的缓存做的不错