LevelDB 学习笔记 —— utils

本文深入探讨了LevelDB中的ShardedLRU缓存机制,分析了其与GDBM的相似之处及不同之处,同时介绍了LRUHandle的巧妙设计及其在缓存管理中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LevelDB是谷歌两个大牛写的KV数据库,相信有很多人已经听过它的名字了。具体背景不做介绍。

今天看的是它的util文件夹,由于以前看过了,所以对其中的代码并不是很陌生,但还是卡在了cache.cc上。

其余的util文件其实没什么多大的难度,arena.cc创建了一个简单的内存分配器,comparator.cc由于还没有接触LevelDB的内部逻辑,可以不用看。histogram.cc是柱状图的实现,可能是test时用的,对于理解LevelDB没有多大影响,可以不看。还剩下status.cc,它解决了函数返回值的问题,基本结构是四个字节的长度+一个字节的code+错误类型的string表示+: +具体信息,自己用一下也很快能明白了。



cache.cc 
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还有两个亮点:


首先说结构上的亮点,在ShardedLRU中,htable和lru链表中使用同样的结构,那就是LRUHandle。这有什么亮点呢?回想nessDB1.0的llru,会发现它的htable和lru使用了不同的节点,然后我们可以看看它的具体插入操作:如果使用了一个lru中不存在的节点,这会将链表中的最后一个node“挤”出去,由于lru中不再有这个节点,htable中也需要移除这个节点,于是便有了ht_remove操作,ht_remove会像ht_get一样查询整个htable。这到底有没有必要?没有。既然节点存在于lru中了,那么它必然存在于htable中,所以我们需要做的就是找到这个节点在htable中的位置,直接删除它。ShardedLRU给出了答案,那就是htable和lru存储同样的节点,使用节点的“多视图”结构。也就是说,节点会记录它自己在htable中的位置(next指针),和在lru链表中的位置(next和prev指针),这样当我们需要删除lru中的末尾节点时,就可以直接对htable进行操作,同样,当在htable中查到该节点时,也可以直接使用查找结果进行lru的更新。但是不好的一点是,nessDB1.0中的htable可以换成set以增加查找速度,但是ShardedLRU则不能了,因为在你删除lru节点时,无法得知set中节点的父子节点指针,或许set应该修改一下,像bimap一样使用context,在set的元素中增加context域就能解决这个问题了。

在代码上的编码技巧就是class HandleTable的FindPointer函数,它返回的是一个二级指针,然后我们使用这个二级指针进行添加和删除操作。然后具体的思路解析呢?可以参看酷壳的这篇文章: http://coolshell.cn/articles/8990.html 上面详细介绍了C语言中二级指针删除链表的思路以及原理。今天花了些时间画图模拟过程,也终于把它搞懂了。简单来说,应该记住的是LRUHandle** 到底是什么?见下图,其中ptr是一个LRUHandle** 的变量。一般地,可以将对链表的操作分成两种结构。


一种是链表头head初始化为一个dummy节点,判断是否为空或者到达末尾的语句是node == dummy,插入时直接向dummy节
点后插入就可以了,删除时,如果没有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文件夹就这么多了。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值