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