cache2go – cachetable原始碼分析

cloudgeek發表於2018-06-19

 

今天我們來看cachetable.go這個原始碼檔案,除了前面介紹過的主要資料結構CacheTable外還有如下2個型別:

下面先看剩下2個型別是怎麼定義的:

CacheItemPair非常簡單,註釋一句話講的很清楚,是用來對映key到訪問計數的

  CacheItemPairList明顯就是一個CacheItemPair組成的“列表”,在go中對應的就是切片,繫結到CacheItemPairList型別的方法有3個,Swap和Len太直觀了,不再贅述,Less方法判斷CacheItemPairList中的第i個CacheItemPair和第j個CacheItemPair的AccessCount大小關係,前者大則返回true,反之false。邏輯是這樣,用處我們在後面具體看(和自定義排序相關,後面我們會講到這裡的玄機)。

  • CacheTable型別繫結的方法

這個原始碼檔案中除了上面講到的部分外就只剩下CacheTable型別繫結的方法了,先看一下有多少:

 下面一個個來看吧~


 1.

Count()方法返回的是指定CacheTable中的item條目數,這裡的table.items是一個map型別,len函式返回map中元素數量


 2.

Foreach方法接收一個函式引數,方法內遍歷了items,把每個key和value都丟給了trans函式來處理,trans函式的引數列表是key interface{}, item *CacheItem,分別對應任意型別的key和CacheItem型別的快取條目的引用。


3.

  先看一下引數列表:f func(interface{}, …interface{}) *CacheItem

  形參的意思是:形參名:f,形參型別:func(interface{}, …interface{}) *CacheItem

  形參型別是一個函式型別,這個函式的引數是一個key和不定數目的argument,返回值是CacheItem指標。

  然後在SetDataLoader中將這個函式f丟給了table的loadData屬性。loadData所指向的方法是什麼時候被呼叫?註釋說which will be called when trying to access a non-existing key.也就是訪問一個不存在的key時會呼叫到。也就是說當訪問一個不存在的key時,需要呼叫一個方法,這個方法通過SetDataLoader設定,方法的實現由使用者來自定義。


4.

  SetAddedItemCallback方法也很直觀,當新增一個Item到快取表中時會被呼叫的一個方法,繫結到CacheTable.addedItem上,被繫結的這個方法只接收一個引數,但是這裡的引數變數名呢?為什麼只寫了型別*CacheItem?我去作者的github上提一個issue問問吧~開玩笑開玩笑,這裡其實不需要形參變數的名字,發現沒?

SetAddedItemCallback方法的形參名是f,型別是後面這個函式,也就是說func(*CacheItem)被作為一個型別,這時候假設寫成func(item *CacheItem),這裡的item用得到嗎?看下面這個例子就清晰了:

1func main() {
2    var show func(int)
3    show = func(num int)
 { fmt.Println(num) }
4    show(123)
5}

  如上所示,定義變數show為func(int)型別的時候不需要形參變數。這個地方明白的看起來很簡單,一時沒有想明白的可能心中會糾結一會~

ok,也就是說SetAddedItemCallback方法設定了一個回撥函式,當新增一個CacheItem的時候,同時會呼叫這個回撥函式,這個函式可以選擇對CacheItem做一些處理,比如打個日誌啊什麼的。


 5.

看到這裡應該很輕鬆了,和上面的回撥函式一樣,這個方法是設定刪除Item的時候呼叫的一個方法。


 6.

這個就不需要講解了,把一個logger例項丟給table的logger屬性。日誌相關的知識點我會在golang專題中詳細介紹。

 


7.

expirationCheck方法比較長,分析部分放在一起有點臃腫,所以我選擇了原始碼加註釋的方式來展示,每一行程式碼和相應的含義如下:

 1// Expiration check loop, triggered by a self-adjusting timer.
2// 【由計時器觸發的到期檢查】
3func (table *CacheTable) expirationCheck() {
4    table.Lock()
5    //【計時器暫停】
6    if table.cleanupTimer != nil {
7        table.cleanupTimer.Stop()
8    }
9    //【計時器的時間間隔】
10    if table.cleanupInterval > 0 {
11        table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
12    } else {
13        table.log("Expiration check installed for table", table.name)
14    }
15
16    // To be more accurate with timers, we would need to update `now` on every
17    // loop iteration. Not sure it`s really efficient though.
18    //【當前時間】
19    now := time.Now()
20    //【最小時間間隔,這裡暫定義為0,下面程式碼會更新這個值】
21    smallestDuration := 0 * time.Second
22    //【遍歷一個table中的items】
23    for key, item := range table.items {
24        // Cache values so we don`t keep blocking the mutex.
25        item.RLock()
26        //【設定好的存活時間】
27        lifeSpan := item.lifeSpan
28        //【最後一次訪問的時間】
29        accessedOn := item.accessedOn
30        item.RUnlock()
31
32        //【存活時間為0的item不作處理,也就是一直存活】
33        if lifeSpan == 0 {
34            continue
35        }
36        //【這個減法算出來的是這個item已經沒有被訪問的時間,如果比存活時間長,說明過期了,可以刪了】
37        if now.Sub(accessedOn) >= lifeSpan {
38            // Item has excessed its lifespan.
39            //【刪除操作】
40            table.deleteInternal(key)
41        } else {
42            // Find the item chronologically closest to its end-of-lifespan.
43            //【按照時間順序找到最接近過期時間的條目】
44            //【如果最後一次訪問的時間到當前時間的間隔小於smallestDuration,則更新smallestDuration】
45            if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
46                smallestDuration = lifeSpan - now.Sub(accessedOn)
47            }
48        }
49    }
50
51    // Setup the interval for the next cleanup run.
52    //【上面已經找到了最近接過期時間的時間間隔,這裡將這個時間丟給了cleanupInterval】
53    table.cleanupInterval = smallestDuration
54    //【如果是0就不科學了,除非所有條目都是0,那就不需要過期檢測了】
55    if smallestDuration > 0 {
56        //【計時器設定為smallestDuration,時間到則呼叫func這個函式】
57        table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
58            //這裡並不是迴圈啟動goroutine,啟動一個新的goroutine後當前goroutine會退出,這裡不會引起goroutine洩漏。
59            go table.expirationCheck()
60        })
61    }
62    table.Unlock()
63}

expirationCheck方法無非是做一個定期的資料過期檢查操作,到目前為止這是專案中最複雜的一個方法,下面繼續看剩下的部分。


8.

  如上圖所示,剩下的方法中劃紅線三個互相關聯,我們放在一起看。

  這次自上而下分析,明顯Add和NotFoundAdd方法會呼叫addInternal方法,所以我們先看Add和NotFoundAdd方法。

先看Add()

  註釋部分說的很清楚,Add方法新增一個key/value對到cache,三個引數除了key、data、lifeSpan的含義我們在第一講分析CacheItem型別的時候都已經介紹過。

  NewCacheItem函式是cacheitem.go中定義的一個建立CacheItem型別例項的函式,返回值是*CacheItem型別。Add方法建立一個CacheItem型別例項後,將該例項的指標丟給了addInternal方法,然後返回了該指標。addInternal我們後面再看具體做了什麼。


 9.

  大家注意到沒有,這裡的註釋有一個單詞寫錯了,they key應該是the key。

  這個方法的引數和上面的Add方法是一樣一樣的,含義無需多說,方法體主要分2個部分:

  開始的if判斷是檢查items中是否有這個key,存在則返回false;後面的程式碼自然就是不存在的時候執行的,建立一個CacheItem型別的例項,然後呼叫addInternal新增item,最後返回true;也就是說這個函式返回true是NotFound的情況。

  ok,下面就可以看看addInternal這個方法幹了啥了。


 10.

  這個方法無非是將CacheItem型別的例項新增到CacheTable中。方法開頭的註釋告訴我們呼叫這個方法前需要加鎖,函式體前2行做了一個打日誌和賦值操作,很好理解,然後將table.cleanupInterval和table.addedItem儲存到區域性變數,緊接著釋放了鎖。

  後面的if部分呼叫了addedItem這個回撥函式,也就是新增一個item時需要呼叫的函式。最後一個if判斷稍微繞一點;

  if的第一個條件:item.lifeSpan > 0,也就是當前item設定的存活時間是正數;然後&& (expDur == 0 || item.lifeSpan < expDur),expDur儲存的是table.cleanupInterval,這個值為0也就是還沒有設定檢查時間間隔,或者item.lifeSpan < expDur也就是設定了,但是當前新增的item的lifeSpan要更小,這個時候就觸發expirationCheck執行。這裡可能有點繞,要注意lifeSpan是一個item的存活時間,而cleanupInterval是對於一個table來說觸發檢查還剩餘的時間,如果前者更小,那麼就說明需要提前出發check操作了。


 11.

剩下的不多了,我們再看一組刪除相關的方法

還是上面的套路,先看上層的呼叫者,當然就是Delete

 

  收一個key,呼叫deleteInternal(key)來完成刪除操作,這裡實在沒有啥可講的了,我們來看deleteInternal方法是怎麼寫的


 12.

deleteInternal方法我也用詳細註釋的方式來解釋吧~

 1func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
2    r, ok := table.items[key]
3    //【如果table中不存在key對應的item,則返回一個error】
4    //【ErrKeyNotFound在errors.go中定義,是errors.New("Key not found in cache")】
5    if !ok {
6        return nil, ErrKeyNotFound
7    }
8
9    // Cache value so we don`t keep blocking the mutex.
10    //【將要刪除的item快取起來】
11    aboutToDeleteItem := table.aboutToDeleteItem
12    table.Unlock()
13
14    // Trigger callbacks before deleting an item from cache.
15    //【刪除操作執行前呼叫的回撥函式,這個函式是CacheTable的屬性,對應下面的是aboutToExpire是CacheItem的屬性】
16    if aboutToDeleteItem != nil {
17        aboutToDeleteItem(r)
18    }
19
20    r.RLock()
21    defer r.RUnlock()
22    //【這裡對這條item加了一個讀鎖,然後執行了aboutToExpire回撥函式,這個函式需要在item剛好要刪除前執行】
23    if r.aboutToExpire != nil {
24        r.aboutToExpire(key)
25    }
26
27    table.Lock()
28    //【這裡對錶加了鎖,上面已經對item加了讀鎖,然後這裡執行delete函式刪除了這個item】
29    //【delete函式是專門用來從map中刪除特定key指定的元素的】
30    table.log("Deleting item with key", key, "created on", r.createdOn, "and hit", r.accessCount, "times from table", table.name)
31    delete(table.items, key)
32
33    return r, nil
34}

 13.

萬里長征最後幾步咯~

最後5(Not打頭這個我們說過了)個方法目測不難,我們一個一個來過,先看Exists

這裡我是想說:來,我們略過吧~

算了,為了教程的完整性,還是簡單說一下,讀鎖的相關程式碼不需要說了,剩下的只有一行:

_, ok := table.items[key]

這裡如果key存在,ok為true,反之為false,就是這樣,簡單吧~


 14.

Value()方法講解,看註釋吧~

 1// Value returns an item from the cache and marks it to be kept alive. You can
2// pass additional arguments to your DataLoader callback function.
3func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
4    table.RLock()
5    r, ok := table.items[key]
6    //【loadData在load一個不存在的資料時被呼叫的回撥函式】
7    loadData := table.loadData
8    table.RUnlock()
9
10    //【如果值存在,執行下面操作】
11    if ok {
12        // Update access counter and timestamp.
13        //【更新accessedOn為當前時間】
14        r.KeepAlive()
15        return r, nil
16    }
17
18    //【這裡當然就是值不存在的時候了】
19    // Item doesn`t exist in cache. Try and fetch it with a data-loader.
20    if loadData != nil {
21        //【loadData這個回撥函式是需要返回CacheItem型別的指標資料的】
22        item := loadData(key, args...)
23        if item != nil {
24            //【loadData返回了item的時候,萬事大吉,執行Add】
25            table.Add(key, item.lifeSpan, item.data)
26            return item, nil
27        }
28        //【item沒有拿到,那就只能返回nil+錯誤資訊了】
29        //【ErrKeyNotFoundOrLoadable是執行回撥函式也沒有拿到data的情況對應的錯誤型別】
30        return nil, ErrKeyNotFoundOrLoadable
31    }
32
33    //【這個return就有點無奈了,在loadData為nil的時候執行,也就是直接返回Key找不到】
34    return nil, ErrKeyNotFound
35}

 15.

從註釋可以看出來這個函式就是清空資料的作用,實現方式簡單粗暴,讓table的items屬性指向一個新建的空map,cleanup操作對應的時間間隔設定為0,並且計時器停止。這裡也可以得到cleanupInterval為0是什麼場景,也就是說0不是代表清空操作死迴圈,間隔0秒就執行,而是表示不需要操作,快取表還是空的。


16.

這個MostAccessed方法有點意思,涉及到sort.Sort的玩法,具體看下面註釋:

 1// MostAccessed returns the most accessed items in this cache table
2//【訪問頻率高的count條item全部返回】
3func (table *CacheTable) MostAccessed(count int64) []*CacheItem {
4    table.RLock()
5    defer table.RUnlock()
6    //【這裡的CacheItemPairList是[]CacheItemPair型別,是型別不是例項】
7    //【所以p是長度為len(table.items)的一個CacheItemPair型別的切片型別
8    p := make(CacheItemPairList, len(table.items))
9    i := 0
10    //【遍歷items,將Key和AccessCount構造成CacheItemPair型別資料存入p切片】
11    for k, v := range table.items {
12        p[i] = CacheItemPair{k, v.accessCount}
13        i++
14    }
15    //【這裡可以直接使用Sort方法來排序是因為CacheItemPairList實現了sort.Interface介面,也就是Swap,Len,Less三個方法】
16    //【但是需要留意上面的Less方法在定義的時候把邏輯倒過來了,導致排序是從大到小的】
17    sort.Sort(p)
18
19    var r []*CacheItem
20    c := int64(0)
21    for _, v := range p {
22        //【控制返回值數目】
23        if c >= count {
24            break
25        }
26
27        item, ok := table.items[v.Key]
28        if ok {
29            //【因為資料是按照訪問頻率從高到底排序的,所以可以從第一條資料開始加】
30            r = append(r, item)
31        }
32        c++
33    }
34
35    return r
36}

17.

最後一個方法了,哇咔咔,好長啊~~~

  這個函式也沒有太多可以講的,為了方便而整的內部日誌函式

  都看懂了嗎?下一講我們會一口氣把cache.go和examples裡全部內容放在一起講完,也就是完成整個專案的分析。3講看完之後你肯定就對cache2go這個專案有一個全面的瞭解了!

 

相關文章