cache2go - cachetable源碼分析


 

今天我們來看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這個項目有一個全面的了解了!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM