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

