带你走进缓存世界(3):缓存原理

        上次我们了解了缓存的基本使用技能,也知道为什么要用缓存,但只是单单谈到了缓存的优势的一点:就是缓存避免的重复性的耗时操作,提高系统性能。其实,如果缓存使用不当,会适得其反。为了避免这种情况的发生,我们更适合了解下缓存的原理。虽然缓存不仅仅是指缓存在内存里的数据,但本节还是以内存为主。
        假如说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;
}
posted @ 2011-08-01 14:46  君之蘭  阅读(836)  评论(0编辑  收藏  举报