上次我们了解了缓存的基本使用技能,也知道为什么要用缓存,但只是单单谈到了缓存的优势的一点:就是缓存避免的重复性的耗时操作,提高系统性能。其实,如果缓存使用不当,会适得其反。为了避免这种情况的发生,我们更适合了解下缓存的原理。虽然缓存不仅仅是指缓存在内存里的数据,但本节还是以内存为主。
假如说A市有1000万人口,我们要根据某个身份证号码,查出这个人的资料,该如何做呢?
有两种做法:
1、把这些数据录入数据库,然后给 身份证 建立唯一索引,然后查询 身份证 = xxx 的用户
2、遍历所有用户,返回 身份证 = xxx 的数据
第一种是我们最常见的办法,当然也是比较现实的做法,但要依赖与某数据库。第二种并没有提到在哪里遍历,或许在内存里,也就是说或许我们已经把所有的资料放在内存里了。你们说哪一种速度快呢?
速度快与慢主要是看访问量,如果访问量小的话,两个没有区别,如果访问量非常大,已经超越了数据库的承受能力时,第一种或许就不合适了,那第二种呢? 更不合适!因为每一个查询都要遍历1000万个资料(最差情况),可想而知cpu的压力巨大。毕竟数据建立了索引,扫描的数据可能只有1条(聚集索引),时间耗费只是索引树(b+tree)的查找,而第二种却是每个都1000万条。也就是第一种的时间复杂度是O(logx(n)),第二种是O(n)。这就是典型的缓存失败案例。
如何解决这个问题?
解决问题自然是从数据的结构入手,数据库之所以快就是采用的树结构(多路平衡树),这是一个数据和存储都比较均衡的结构,实际上还有更快的结构,那就是缓存常用的hashtable(哈希表/散列表)结构,看到hashtable,大家应该立刻会想到key/value键值对,是的,键值对更快,hashtable是一种快速定位的数据结构,他查询数据的时间复杂度是O(1),当然实际上是有所偏差的,因为牵扯到hash值的计算和hash值冲突的问题。.NET的对象之根是Object,Object类有一个GetHashCode方法,所以我们的对象默认都有该方法。此方法返回一个int类型的值,即为该对象的hash值。比如,任意一个string字符串,也具备GetHashCode的方法。我们上节中用到了的缓存类的key都是string,每个string都对应了一个hash值,也就是key的hash值对应value的具体位置。假设我们把一个hashtable想成是一个数组,那么假如我们知道了某个元素的下标,想要获取这个元素是不是可以直接Array[下标],就可以了,所以,在此你完全可以把hash值想象成数组下标,所以我们获取value的时候只要提供key,便可以直接计算出hash值(下标),然后直接获取value。
下面的图片画的是hashtable的具体存储结构。
数字为数组下标,entry为具体KeyValue对象,箭头是value是指向下一个entry的指针
可以看出有的地方没有数据,有的地方有多条数据,大部分地方是1条数据,每条数据都对应了一个hash值。
这种情况说明了几点情况:
1、一个位置对应了多个entry,这说明不同的key的hash值可能是一个。
2、有的位置没有entry,这说明key所对应的hash值并非是紧密排列的,会造成一定的空间浪费。
3、大部分是有一个entry,这说明获取hash值的算法比较合理,使的大部分key的hash值都不同。
Entry是一个结构体,里面包含了hash值,key/Value后面的Entry(即next,主要用于hash冲突)
private struct Entry { public int hashCode; public int next; public TKey key; public TValue value; }
GetHashCode的具体算法是GetHashCode方法是虚方法,所以我们在定义类时可以按自己的算法计算hash值。优秀的hash算法是减少冲突,均与分布。下面是.NET4.0 string类的GetHashCode代码:
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecuritySafeCritical] public override unsafe int GetHashCode() { fixed (char* str = ((char*)this)) { char* chPtr = str; int num = 0x15051505; int num2 = num; int* numPtr = (int*)chPtr; for (int i = this.Length; i > 0; i -= 4) { num = (((num << 5) + num) + (num >> 0x1b)) ^ numPtr[0]; if (i <= 2) break; num2 = (((num2 << 5) + num2) + (num2 >> 0x1b)) ^ numPtr[1]; numPtr += 2; } return (num + (num2 * 0x5d588b65)); } }
大家明白大致原理即可,如果想深究,这里有一篇极好的文章推荐阅读:http://www.cnblogs.com/abatei/archive/2009/06/23/1509790.html
另外:在.NetFramework里还有一个常用个的类Dictionary<TKey,TValue>,其和Hashtable相比不仅仅是泛型的支持,其内部算法也略有不同。
下面是Dictionary的Insert和Get方法,可以上面上链接里的hashtable做一下对比:
private void Insert(TKey key, TValue value, bool add) { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (this.buckets == null) { this.Initialize(0); } int num = this.comparer.GetHashCode(key) & 2147483647;//保证hash值为正直 int num2 = num % this.buckets.Length;//保证hash值在容器内 //此处循环是先直接定位到hash值下标的Entry和其next entry for (int i = this.buckets[num2]; i >= 0; i = this.entries[i].next) { //如果该元素已存在value且key一致 if (this.entries[i].hashCode == num && this.comparer.Equals(this.entries[i].key, key)) { //如果是添加则抛出已添加异常 if (add) { ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate); } //更新该Entry this.entries[i].value = value; this.version++; return; } } int num3; //这里是插入所用,先判断是否有空闲的Entry(可能之前被remove过的) if (this.freeCount > 0) { //如果有,则直接把freeList最顶部的下标(也就是上前面的一个空闲拿出来用) num3 = this.freeList; this.freeList = this.entries[num3].next; this.freeCount--; } else { if (this.count == this.entries.Length) { this.Resize(); num2 = num % this.buckets.Length; } num3 = this.count; this.count++; } //此下标构的Entry赋值新的k/v; this.entries[num3].hashCode = num; this.entries[num3].next = this.buckets[num2]; this.entries[num3].key = key; this.entries[num3].value = value; this.buckets[num2] = num3; this.version++; }
public TValue this[TKey key] { get { int num = this.FindEntry(key); if (num >= 0) { return this.entries[num].value; } ThrowHelper.ThrowKeyNotFoundException(); return default(TValue); } [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] set { this.Insert(key, value, false); } }
private int FindEntry(TKey key) { if (key == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key); } if (this.buckets != null) { int num = this.comparer.GetHashCode(key) & 2147483647; for (int i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next) { if (this.entries[i].hashCode == num && this.comparer.Equals(this.entries[i].key, key)) { return i; } } } return -1; }