LevelDB是谷歌两个大牛写的KV数据库,相信有很多人已经听过它的名字了。具体背景不做介绍。
今天看的是它的util文件夹,由于以前看过了,所以对其中的代码并不是很陌生,但还是卡在了cache.cc上。
其余的util文件其实没什么多大的难度,arena.cc创建了一个简单的内存分配器,comparator.cc由于还没有接触LevelDB的内部逻辑,可以不用看。histogram.cc是柱状图的实现,可能是test时用的,对于理解LevelDB没有多大影响,可以不看。还剩下status.cc,它解决了函数返回值的问题,基本结构是四个字节的长度+一个字节的code+错误类型的string表示+: +具体信息,自己用一下也很快能明白了。
LevelDB的缓存使用Sharded LRU,这个名字念起来有些熟悉,因为nessDB1.0的时候才碰到过Level LRU,都是对普通LRU的优化吧。ShardedLRU将哈希值分为16个范围,也就是哈希值的前4位映射。感觉又十分熟悉了?没错,就好像GDBM的dir一样,首先根据哈希值的前n位来定位htable,这两个思想都是一样的,目的当然也都是为了提高运行速度,并且可以适当减少htable的分裂。并且,从线程安全的角度考虑,将cache分为固定的16个部分,可以用16个锁来对整个cache进行操作,从而避免了对互斥区访问的延迟。cache.cc的htable相对来说比较简单,因为它不是双重哈希,而是一个哈希链表,所以不存在dir的重定位问题,散列冲突后直接使用链表解决就行了。
但是仔细想想,ShardedLRU的htable跟GDBM的htable之间到底有什么区别和联系呢?ShardedLRU的htable的长度是倍增的,但是Shard的个数也就是dir的比特数是不变的,而GDBM的htable的长度是不变的,dir的比特数是增加的,为啥?原因是散列冲突的解决方案不同,因为GDBM使用的冲突解决方案是向前加1,所以需要保证htable的长度不能太长,以避免删除时造成计算拥堵。而ShardedLRU的htable使用链表解决冲突,倍增链表时也是只对一个shard对应的htable进行更新,跟GDBM是一样的。我们可以看到两种htable结构的优点和缺点:就GDBM的htable来说,它的冲突解决直接导致它在删除时需要进行“坑的填埋”工作,在插入和查找时进行地毯式的搜寻,而链表方式则可以不用如此繁琐。然后看ShardedLRU的优缺点:它的优点就是使用链表来解决冲突,但是它的缺点是shard是固定的,这样当一个htable的长度过大时,再次resize会使得工作变得困难——重新计算节点在htable中的分布时会计算许多不必要的节点。不如将一个htable的长度限制在一个范围之内。这样,我得出一个结合两者优点,而避免两者缺点的折中方案:使用GDBM的dir来进行htable的分裂,并使用ShardedLRU htable的链表解决冲突。
好了,这就是ShardedLRU的整体结构,从整体结构上来看,并不难理解,但是在编码和结构使用上,cache.cc还有两个亮点:
在代码上的编码技巧就是class HandleTable的FindPointer函数,它返回的是一个二级指针,然后我们使用这个二级指针进行添加和删除操作。然后具体的思路解析呢?可以参看酷壳的这篇文章: http://coolshell.cn/articles/8990.html 上面详细介绍了C语言中二级指针删除链表的思路以及原理。今天花了些时间画图模拟过程,也终于把它搞懂了。简单来说,应该记住的是LRUHandle** 到底是什么?见下图,其中ptr是一个LRUHandle** 的变量。一般地,可以将对链表的操作分成两种结构。
点后插入就可以了,删除时,如果没有prev指针,则需要保存一个prev节点,这个dummy在这里就是LRUHandle的结构了,它占的字节数是sizeof(LRUHandle);这种表示的缺点就是dummy占用了不必要的空间,删除需要记录prev节点。
另一种,head初始化为NULL,判断语句为node == NULL,插入时首先要判断head是不是NULL,然后再进行其它操作,删除时同样需要保存一个prev节点,但它所占的字节数就是sizeof(void*)。这种操作的缺点是在删除时也需要记录prev节点,在插入时判断head节点是否为空。那么到底有没有一种更简单的方法呢?有,那就是使用二级指针。LRUHandle** ptr,这个*ptr有时代表head中的那个LRUHandle*,有时又代表一个LRUHandle中的next_hash域的值,(*ptr)->表示啥呢?首先*ptr是一个LRUHandle*,它是next_hash域的值,也就是一个地址,再使用->操作符,实际上是在找*ptr地址(也就是一个LRUHandel)中的值。修改*ptr其实就是修改next_hash域的值,那么*ptr = addr其实就是prev->next_hash = addr,这样删除一个节点的代码就应该是*ptr/*next_hash内存块的值*/ = (*ptr)->/*ptr处的node,也就是需要删除的节点*/next_hash/*下一个节点*/。
需要注意的是,当FindePointer没有找到对应的节点,也就是*ptr为NULL时到底是怎样的,*ptr为NULL有两种情况,一种是这个slot处的链表是空的,一种是到达了链表的末尾。
util文件夹就这么多了。