LevelDB 學習筆記 —— utils

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文件夾就這麼多了。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章