Mango?Cache緩存管理庫TinyLFU源碼解析
介紹
據(jù)官方所述,mango Cache是對Guava Cache基于go的部分實(shí)現(xiàn),同時(shí)mangoCache參考了Caffeine以及go-tinylfu.
支持以下緩存管理策略:
- LRU
- Segmented LRU(默認(rèn))
- TinyLFU(實(shí)驗(yàn)性)
本文將從源碼對其進(jìn)行解析,重點(diǎn)將放在loadingCache(一種可以自定義如何載入新內(nèi)存的cache)上面.
整體架構(gòu)
mango Cache的主體功能由localCache結(jié)構(gòu)體(local.go)實(shí)現(xiàn),
type localCache struct { cache cache // 真正存放數(shù)據(jù)的地方 expireAfterAccess time.Duration //用戶自定義的過期以及刷新時(shí)間 expireAfterWrite time.Duration refreshAfterWrite time.Duration policyName string //緩存管理策略 onInsertion Func //鉤子函數(shù) onRemoval Func loader LoaderFunc //自定義加載和重載函數(shù) reloader Reloader stats StatsCounter //狀態(tài)管理器 cap int //緩存容量 // 訪問時(shí)的緩存管理策略 accessQueue policy // 寫入時(shí)的緩存管理策略,只有在expireAfterWrite或refreshAfterWrite被設(shè)置以后才啟用 writeQueue policy // 用于processEntries的事件隊(duì)列 events chan entryEvent // 記錄距離上一次寫期間發(fā)生的讀次數(shù),用于判斷是否進(jìn)行清掃 readCount int32 // 用于關(guān)閉由localCache創(chuàng)建的協(xié)程 closing int32 closeWG sync.WaitGroup }
真正存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu)體是cache(policy.go),所有緩存數(shù)據(jù)都存儲(chǔ)在其中
type cache struct { size int64 // 緩存大小 segs [segmentCount]sync.Map // 分片sync.map, map[key]*entry }
entry是存放一個(gè)緩存數(shù)據(jù)的單元
type entry struct { hash uint64 //key的hash值 accessTime int64 writeTime int64 invalidated int32 //是否有效 loading int32 //是否處于載入中狀態(tài) key Key value atomic.Value // 存儲(chǔ)的值 // 下面的屬性只被緩存管理策略操作,所以不需要原子性的訪問 accessList *list.Element //此entry目前所在的訪問鏈表中的位置 writeList *list.Element //此entry目前所在的寫入鏈表中的位置 listID uint8 //此entry目前所在的緩存段(SLRU) }
localCache使用事件隊(duì)列來管理并發(fā)讀寫事件,事件隊(duì)列是一個(gè)chan,其中流轉(zhuǎn)著entryEvent(即數(shù)據(jù)以及對應(yīng)的緩存事件),localCache通過processEntries協(xié)程來處理各種讀寫事件.之后將詳細(xì)講解mango Cache是如何進(jìn)行讀寫操作的.
localCache的write、access等操作底層是通過操作cache、accessQueue以及writeQueue從而實(shí)現(xiàn)的,而localCache還會(huì)負(fù)責(zé)清掃過期數(shù)據(jù)的工作.
前面提到了mango Cache支持諸如LRU、SLRU以及TinyLFU等緩存管理策略,其在localCache中即為accessQueue以及writeQueue,負(fù)責(zé)對緩存的淘汰準(zhǔn)入等操作.
初始化流程
mango Cache提供了兩種cache---普通Cache以及LoadingCache,這兩者都是接口,而localCache實(shí)現(xiàn)了這兩個(gè)接口,由于LoadingCache繼承了普通Cache,因此本文只講解LoadingCache.
func NewLoadingCache(loader LoaderFunc, options ...Option) LoadingCache { c := newLocalCache() c.loader = loader for _, opt := range options { opt(c) } c.init() return c }
NewLoadingCache函數(shù)先初始化一個(gè)LoadingCache,然后根據(jù)用戶傳入的自定義載入函數(shù)和一些配置來初始化LoadingCache.配置包括注冊插入或者刪除時(shí)觸發(fā)的鉤子函數(shù)以及過期時(shí)間等等.然后調(diào)用localCache.init.
func (c *localCache) init() { c.accessQueue = newPolicy(c.policyName) c.accessQueue.init(&c.cache, c.cap) if c.expireAfterWrite > 0 || c.refreshAfterWrite > 0 { c.writeQueue = &recencyQueue{} } else { c.writeQueue = discardingQueue{} } c.writeQueue.init(&c.cache, c.cap) c.events = make(chan entryEvent, chanBufSize) c.closeWG.Add(1) go c.processEntries() }
localCache.init會(huì)根據(jù)用戶傳入的緩存管理策略名字來初始化accessQueue然后根據(jù)是否有寫過期和寫刷新配置來決定是否初始化寫入隊(duì)列.接著創(chuàng)建事件隊(duì)列并開啟事件處理協(xié)程.到此為止,cache啟動(dòng)完成.
讀流程
LoadingCache的Get操作可以通過key獲取緩存值,其流程為:
先從主緩存中查詢entry
若未查詢到entry,則記錄miss并且調(diào)用用戶自定義的load方法加載緩存值并返回
若查詢到entry,先檢查是否過期
- 若過期且沒有設(shè)置loader則直接向事件處理協(xié)程發(fā)送eventDelete
- 若過期但設(shè)置了loader,則異步更新entry值
若沒有過期則更新訪問時(shí)間并向事件處理協(xié)程發(fā)送eventAccess然后記錄命中
最后返回entry
func (c *localCache) Get(k Key) (Value, error) { en := c.cache.get(k, sum(k)) //計(jì)算key的hash并查詢該key if en == nil { c.stats.RecordMisses(1) return c.load(k) } // 檢查entry是否需要更新 now := currentTime() if c.isExpired(en, now) { if c.loader == nil { //如果沒有設(shè)置加載器則直接刪除該entry c.sendEvent(eventDelete, en) } else { //對于loadingCache,我們不刪除這個(gè)entry //而是把它暫時(shí)留在緩存中,所以用戶依舊可以讀取到舊的緩存值 c.setEntryAccessTime(en, now) c.refreshAsync(en) } c.stats.RecordMisses(1) } else { c.setEntryAccessTime(en, now) c.sendEvent(eventAccess, en) c.stats.RecordHits(1) } return en.getValue(), nil }
需要注意一下這里的refreshAsync函數(shù):
func (c *localCache) refreshAsync(en *entry) bool { if en.setLoading(true) { // 如果此entry沒有在加載 if c.reloader == nil { go c.refresh(en) } else { c.reload(en) } return true } return false }
如果沒有用戶設(shè)置的重載器,就異步執(zhí)行refresh,refresh函數(shù)實(shí)際上就是對entry進(jìn)行加載.
而如果有重載器那么就同步執(zhí)行用戶自定義的reload函數(shù).
寫流程
loadingCache的Put操作與Get操作類似,流程如下:
先去主緩存查詢key是否存在,若查詢到對應(yīng)的entry,那么直接更新entry
若沒有查詢到對應(yīng)的entry,說明其不存在,因此根據(jù)key,value初始化一個(gè)新entry
如果緩存容量足夠,則讓主緩存存儲(chǔ)該entry,此時(shí)會(huì)再次檢查主存中是否有該entry(解決并發(fā)問題)
- 若cen不為空,說明主緩存中已經(jīng)存在該entry,直接修改該entry即可
- 若cen為空,說明主緩存中還不存在該entry,那么就會(huì)在主緩存中存儲(chǔ)該entry
最后向事件處理協(xié)程發(fā)送eventWrite事件
func (c *localCache) Put(k Key, v Value) { h := sum(k) en := c.cache.get(k, h) now := currentTime() if en == nil { en = newEntry(k, v, h) c.setEntryWriteTime(en, now) c.setEntryAccessTime(en, now) // 直接將新value添加進(jìn)緩存(在緩存容量足夠的時(shí)候) if c.cap == 0 || c.cache.len() < c.cap { cen := c.cache.getOrSet(en) if cen != nil { cen.setValue(v) c.setEntryWriteTime(cen, now) en = cen } } } else { // 更新entry en.setValue(v) c.setEntryWriteTime(en, now) } c.sendEvent(eventWrite, en) } //當(dāng)entry在緩存中存在,則返回該entry,否則存儲(chǔ)該entry func (c *cache) getOrSet(v *entry) *entry { seg := c.segment(v.hash) en, ok := seg.LoadOrStore(v.key, v) if ok { return en.(*entry) } atomic.AddInt64(&c.size, 1) return nil }
事件處理機(jī)制
主流程
mango Cache通過entryEvent chan以及processEntries協(xié)程來處理并發(fā)讀寫事務(wù)
緩存事件一共四個(gè),分別為寫入、訪問、刪除以及關(guān)閉.每個(gè)業(yè)務(wù)協(xié)程通過向localCache的events chan發(fā)送entryEvent通知事件處理協(xié)程,進(jìn)而實(shí)現(xiàn)并發(fā)讀寫.
而processEntries協(xié)程內(nèi),會(huì)不斷從events chan內(nèi)取出entryEvent并執(zhí)行對應(yīng)的操作,在write、access以及delete操作后會(huì)執(zhí)行清理工作(具體清掃工作由expireEntries函數(shù)執(zhí)行)
type event uint8 const ( eventWrite event = iota eventAccess eventDelete eventClose ) type entryEvent struct { entry *entry event event } func (c *localCache) processEntries() { defer c.closeWG.Done() for e := range c.events { switch e.event { case eventWrite: //寫入事務(wù) c.write(e.entry) c.postWriteCleanup() //清理操作 case eventAccess: //訪問事務(wù) c.access(e.entry) c.postReadCleanup() //清理操作 case eventDelete: if e.entry == nil { //InvalidateAll函數(shù)中使用 c.removeAll() } else { c.remove(e.entry) //移除單個(gè)entry } c.postReadCleanup() //清理操作 case eventClose: if c.reloader != nil { // 停止所有refresh工作 c.reloader.Close() } c.removeAll() return } } }
write
由于事件處理機(jī)制對于access、delete和write的操作類似,因此這里只講解較為復(fù)雜的write操作:
首先通過調(diào)用底層訪問隊(duì)列以及寫入隊(duì)列的write方法
觸發(fā)用戶自定義的鉤子函數(shù)
如果write方法返回值不為空,說明有entry被驅(qū)逐,
因此需要從寫入隊(duì)列將其刪除,同時(shí)記錄驅(qū)逐并觸發(fā)用戶自定義的鉤子函數(shù)
func (c *localCache) write(en *entry) { ren := c.accessQueue.write(en) c.writeQueue.write(en) if c.onInsertion != nil { c.onInsertion(en.key, en.getValue()) } if ren != nil { //有entry被驅(qū)逐出了緩存 c.writeQueue.remove(ren) c.stats.RecordEviction() if c.onRemoval != nil { c.onRemoval(ren.key, ren.getValue()) } } }
后面將詳細(xì)講述底層訪問隊(duì)列以及緩存管理是如何實(shí)現(xiàn)的.
清理工作
前面講到過每次進(jìn)行完write、access以及delete操作后會(huì)執(zhí)行清理工作.具體地,write操作會(huì)觸發(fā)postWriteCleanup而access和delete操作會(huì)觸發(fā)postReadCleanup.
postReadCleanup會(huì)根據(jù)當(dāng)前距離上一次寫的read操作次數(shù)是否達(dá)到清理工作閾值來決定是否清理,這個(gè)閾值是64,也就是說每隔64次read操作就會(huì)觸發(fā)一次清理工作
而postWriteCleanup將在每一次write操作之后觸發(fā)
真正的清理工作由expireEntries函數(shù)完成,它一次清理工作最多只會(huì)清理16個(gè)entry避免了對事件處理的長時(shí)間阻塞.
func (c *localCache) postReadCleanup() { if atomic.AddInt32(&c.readCount, 1) > drainThreshold { atomic.StoreInt32(&c.readCount, 0) c.expireEntries() } } // 在添加完entry以后再執(zhí)行 func (c *localCache) postWriteCleanup() { atomic.StoreInt32(&c.readCount, 0) c.expireEntries() } //清理工作函數(shù) func (c *localCache) expireEntries() { remain := drainMax now := currentTime() if c.expireAfterAccess > 0 { expiry := now.Add(-c.expireAfterAccess).UnixNano() c.accessQueue.iterate(func(en *entry) bool { if remain == 0 || en.getAccessTime() >= expiry { // 找到了第一個(gè)沒有過期的entry或者清理entry數(shù)足夠了 return false } c.remove(en) c.stats.RecordEviction() remain-- return remain > 0 }) } if remain > 0 && c.expireAfterWrite > 0 { ... } if remain > 0 && c.loader != nil && c.refreshAfterWrite > 0 { ... } }
緩存管理
localCache在初始化過程中也初始化了緩存管理策略,由于localCache的writeQueue默認(rèn)使用LRU緩存淘汰策略,而accessQueue支持LRU、SLRU以及TinyLFU三種緩存淘汰策略,本節(jié)將著重講解accessQueue.
什么是LRU?
LRU是Least Recently Used的縮寫,即最近最少使用.在mango Cache中LRU依靠go SDK自帶的List(雙向鏈表)實(shí)現(xiàn),新緩存條目會(huì)被插入List頭部,如果List內(nèi)元素達(dá)到容量上限則刪除List尾部的元素,此元素也正是最近最少使用的元素.LRU可以使得主緩存內(nèi)的緩存條目永遠(yuǎn)是最近被訪問的,不是最近訪問的元素將被淘汰.
如果一個(gè)不是經(jīng)常使用的數(shù)據(jù),偶爾或者周期性的被使用,那么該數(shù)據(jù)會(huì)被加到LRU鏈表頭部,而這種不經(jīng)常使用的數(shù)據(jù),放在鏈表頭部,占用了空間;一直等到LRU淘汰完,才會(huì)被剔除鏈表;如果這種數(shù)據(jù)一次性過多,那么鏈表數(shù)據(jù)都是這種無用的數(shù)據(jù),從而會(huì)導(dǎo)致緩存命中率低下,影響系統(tǒng)性能.
什么是SLRU?
Segmented LRU(SLRU)是一種LRU的改進(jìn),主要把在一個(gè)時(shí)間窗口內(nèi)命中至少2次的記錄和命中1次的單獨(dú)存放,這樣就可以把短期內(nèi)較頻繁的緩存元素區(qū)分開來.具體做法上,SLRU包含2個(gè)固定尺寸的LRU,一個(gè)叫Probation段(A1),一個(gè)叫Protection段(A2).新記錄總是插入到A1中,當(dāng)A1的記錄被再次訪問,就把它移到A2,當(dāng)A2滿了需要驅(qū)逐記錄時(shí),會(huì)把驅(qū)逐記錄插入到A1中.
在mango Cache的實(shí)現(xiàn)中,Protection段與Probation段大小比為4:1.
SLRU是mango Cache的默認(rèn)緩存管理策略
什么是TinyLFU?
TinyLFU是一種空間利用率很高的新的數(shù)據(jù)結(jié)構(gòu),可以在較大訪問量的場景下近似的替代LFU的數(shù)據(jù)統(tǒng)計(jì)部分(meta-data),其采用類似布隆過濾器的位計(jì)數(shù)器來實(shí)現(xiàn)對每個(gè)key的訪問次數(shù)的粗略統(tǒng)計(jì).(由于哈希沖突的存在,位計(jì)數(shù)器的結(jié)果將偏大)
對于較為靜態(tài)的訪問分布,各種的緩存管理策略的差異是微不足道的,而對于偏倚較大的負(fù)載場景,TinyLFU體現(xiàn)出了較大的優(yōu)勢.即便是使用了TinyLFU準(zhǔn)入策略進(jìn)行優(yōu)化的LRU也獲得了較大的提升.
如果想要詳細(xì)了解TinyLFU請閱讀論文TinyLFU: A Highly Efficient Cache Admission Policy 以及一種簡略實(shí)現(xiàn)講解LRU、LFU、TinyLFU緩存算法
mango Cache中的TinyLFU
mango Cache的實(shí)現(xiàn)中,實(shí)際上是實(shí)現(xiàn)了改進(jìn)版的TinyLFU也就是Window-TinyLFU,這個(gè)緩存數(shù)據(jù)結(jié)構(gòu)也在論文中有講解,簡而言之就是緩存由window Cache、doorKeeper(布隆過濾器)、counter以及SLRU組成.主緩存使用SLRU驅(qū)逐策略和TinyLFU準(zhǔn)入策略,window Cache僅使用LRU驅(qū)逐策略無準(zhǔn)入策略.
主緩存中的SLRU策略的A1和A2區(qū)域被靜態(tài)劃分開來,80%空間被分配給熱點(diǎn)元素(A2),被驅(qū)逐者則從20%的非熱項(xiàng)(A1)中選取.任何到來的元素總是允許進(jìn)入window Cache, window Cache的淘汰元素有機(jī)會(huì)進(jìn)入主緩存,如果在經(jīng)過TinyLFU準(zhǔn)入策略檢驗(yàn)后被主緩存接受,那么該元素將進(jìn)入主緩存,此時(shí)Window-TinyLFU的淘汰者就是主緩存的淘汰者(從A1段選出),否則該元素將被window Cache淘汰.
type tinyLFU struct { filter bloomFilter // doorkeeper counter countMinSketch // 4bit計(jì)數(shù)器 additions int //目前采樣數(shù)量 samples int //采樣窗口大小 lru lruCache //window Cache slru slruCache //主緩存 }
接下來本文將詳細(xì)講述mango Cache中tinyLFU的具體實(shí)現(xiàn)
counter
counter是實(shí)現(xiàn)TinyLFU的重點(diǎn),對于訪問次數(shù)的粗略統(tǒng)計(jì)就是通過此數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的.
const sketchDepth = 4 //hash次數(shù) type countMinSketch struct { counters []uint64 mask uint32 }
可以看到, counter由一個(gè)uint64類型的數(shù)組和一個(gè)mask組成, 因?yàn)槲覀兪褂玫氖?次hash的4bit計(jì)數(shù)器,因此數(shù)組的每個(gè)元素實(shí)際上是4個(gè)計(jì)數(shù)器,可以通過位操作來實(shí)現(xiàn)訪問.
counter的初始化
func (c *countMinSketch) init(width int) { size := nextPowerOfTwo(uint32(width)) >> 2 //size = (width * 4 * 4) bits / 64 if size < 1 { size = 1 } c.mask = size - 1 if len(c.counters) == int(size) { c.clear() } else { c.counters = make([]uint64, size) } }
我們通過傳入的width確定需要的計(jì)數(shù)器數(shù)量(size大小)
counter的使用
counter最關(guān)鍵的操作就是按hash增加計(jì)數(shù)器值以及根據(jù)hash估算該hash的計(jì)數(shù)器值:
func (c *countMinSketch) add(h uint64) { h1, h2 := uint32(h), uint32(h>>32) for i := uint32(0); i < sketchDepth; i++ { //這里使用折疊法求得hash值 idx, off := c.position(h1 + i*h2) c.inc(idx, (16*i)+off) } } // 根據(jù)給定hash值返回對應(yīng)計(jì)數(shù)器中最小的那個(gè)計(jì)數(shù)器值 func (c *countMinSketch) estimate(h uint64) uint8 { h1, h2 := uint32(h), uint32(h>>32) var min uint8 = 0xFF for i := uint32(0); i < sketchDepth; i++ { idx, off := c.position(h1 + i*h2) count := c.val(idx, (16*i)+off) if count < min { min = count } } return min }
里面的position、val以及inc函數(shù)都是對應(yīng)的位操作,有興趣的話可以進(jìn)一步研究下.
需要注意的是estimate函數(shù)用于求給定hash對應(yīng)計(jì)數(shù)器中最小計(jì)數(shù)器的值,這是因?yàn)橛泄E鲎驳那闆r,因此計(jì)數(shù)器的值始終會(huì)大于等于真實(shí)的訪問次數(shù),因此這里采用多次hash取其中最小值的方法來減少誤差.
同時(shí),為了保證計(jì)數(shù)器的時(shí)效性,counter實(shí)現(xiàn)了新鮮度機(jī)制,將在一定條件下觸發(fā)reset:
//將每個(gè)計(jì)數(shù)器的值減半 func (c *countMinSketch) reset() { for i, v := range c.counters { if v != 0 { c.counters[i] = (v >> 1) & 0x7777777777777777 } } }
lruCache
lruCache是LRU的實(shí)現(xiàn),在mango Cache中是一個(gè)鏈表.
type lruCache struct { cache *cache cap int ls list.List } func (l *lruCache) init(c *cache, cap int) { l.cache = c l.cap = cap l.ls.Init() }
slruCache
slruCache是SLRU的實(shí)現(xiàn),在mango Cache中由兩個(gè)鏈表組成.
const ( protectedRatio = 0.8 ) type slruCache struct { cache *cache probationCap int probationLs list.List protectedCap int protectedLs list.List } func (l *slruCache) init(c *cache, cap int) { l.cache = c l.protectedCap = int(float64(cap) * protectedRatio) l.probationCap = cap - l.protectedCap l.probationLs.Init() l.protectedLs.Init() }
slruCache中,值得注意的點(diǎn)是它的寫入策略:
func (l *slruCache) write(en *entry) *entry { if en.accessList != nil { // entry已存在,直接修改該entry l.markAccess(en) return nil } // 嘗試將新entry加入probation段 cen := l.cache.getOrSet(en) if cen == nil { en.listID = probationSegment //將其加入probation段 en.accessList = l.probationLs.PushFront(en) } else { // 該entry已存在,直接修改它的值 cen.setValue(en.getValue()) cen.setWriteTime(en.getWriteTime()) if cen.accessList == nil { //該entry已載入緩存但是還沒有注冊到SLRU管理的鏈表中 cen.listID = probationSegment cen.accessList = l.probationLs.PushFront(cen) } else { l.markAccess(cen) } } // probation段超出容量但list并沒有超出容量 if l.probationCap > 0 && l.probationLs.Len() > l.probationCap && l.length() > (l.probationCap+l.protectedCap) { // 移除并返回probation段尾部的entry en = getEntry(l.probationLs.Back()) return l.remove(en) } return nil } //記錄訪問情況 func (l *slruCache) markAccess(en *entry) { if en.listID == protectedSegment { // entry位于在protected段 l.protectedLs.MoveToFront(en.accessList) return } // 若entry位于probation段,則將其提升至protected段 en.listID = protectedSegment l.probationLs.Remove(en.accessList) en.accessList = l.protectedLs.PushFront(en) if l.protectedCap > 0 && l.protectedLs.Len() > l.protectedCap { // protected段超出容量限制,則淘汰其尾部的entry將其加入probation段 en = getEntry(l.protectedLs.Back()) en.listID = probationSegment l.protectedLs.Remove(en.accessList) en.accessList = l.probationLs.PushFront(en) } }
這樣一來,就實(shí)現(xiàn)了SLRU的緩存管理策略.
filter
filter是一個(gè)布隆過濾器,這里不展開講解其實(shí)現(xiàn)細(xì)節(jié),只需要了解布隆過濾器可以通過key的hash來確定這個(gè)key是否在filter中,并且其只占用非常小的內(nèi)存.如果想要詳細(xì)了解布隆過濾器,可以參考這篇文章:布隆過濾器
type bloomFilter struct { numHashes uint32 // 每個(gè)元素使用的hash數(shù)量 bitsMask uint32 // 位向量大小 bits []uint64 // 過濾器位向量 } //根據(jù)給定的插入元素?cái)?shù)量以及假陽性率初始化布隆過濾器 func (f *bloomFilter) init(ins int, fpp float64) { ln2 := math.Log(2.0) factor := -math.Log(fpp) / (ln2 * ln2) numBits := nextPowerOfTwo(uint32(float64(ins) * factor)) if numBits == 0 { numBits = 1 } f.bitsMask = numBits - 1 if ins == 0 { f.numHashes = 1 } else { f.numHashes = uint32(ln2 * float64(numBits) / float64(ins)) } size := int(numBits+63) / 64 if len(f.bits) != size { f.bits = make([]uint64, size) } else { f.reset() } }
TinyLFU的初始化
前面介紹了TinyLFU的各個(gè)組件,接下來將詳細(xì)講解TinyLFU是如何進(jìn)行緩存管理的.
const ( //entry所處的緩存段 admissionWindow uint8 = iota probationSegment protectedSegment ) const ( samplesMultiplier = 8 //采樣窗口大小乘數(shù) insertionsMultiplier = 2 //doorkeeper大小乘數(shù) countersMultiplier = 1 //計(jì)數(shù)器大小乘數(shù) falsePositiveProbability = 0.1 //假陽性率 admissionRatio = 0.01 //window Cache占比 ) func (l *tinyLFU) init(c *cache, cap int) { if cap > 0 { // 只在容量有上限時(shí)開啟doorkeeper l.samples = samplesMultiplier * cap //采樣窗口大小 l.filter.init(insertionsMultiplier*cap, falsePositiveProbability) //doorkeeper初始化 l.counter.init(countersMultiplier * cap) //計(jì)數(shù)器初始化 } lruCap := int(float64(cap) * admissionRatio) l.lru.init(c, lruCap) //window Cache初始化 l.slru.init(c, cap-lruCap) //SLRU主存初始化 }
TinyLFU寫入
TinyLFU的寫入流程如下
首先寫入window Cache
如果window Cache出現(xiàn)被淘汰的candidate,則將從SLRU中選取一個(gè)victim(如果SLRU未滿,則不會(huì)產(chǎn)生victim,此時(shí)直接將candidate插入SLRU)
在計(jì)數(shù)器中查詢candidate和victim的訪問次數(shù)
- 若candidate的訪問次數(shù)較大,則將其插入SLRU,淘汰victim
- 否則將candidate淘汰
根據(jù)此流程,我們可以發(fā)現(xiàn)被插入SLRU的entry的訪問次數(shù)一定是較大的,而我們通過計(jì)數(shù)器實(shí)現(xiàn)了對entry訪問次數(shù)的保存,這樣就結(jié)合了LRU以及LFU的優(yōu)點(diǎn)并且沒有占用過多的內(nèi)存,這正是TinyLFU最大的優(yōu)勢所在.
func (l *tinyLFU) write(en *entry) *entry { if l.lru.cap <= 0 { //若容量無限,則直接對SLRU寫入 return l.slru.write(en) } l.increase(en.hash) //entry訪問次數(shù)+1 candidate := l.lru.write(en) //window Cache中被淘汰的entry if candidate == nil { return nil } victim := l.slru.victim() //SLRU中下一個(gè)將被淘汰的entry if victim == nil { return l.slru.write(candidate) } candidateFreq := l.estimate(candidate.hash) victimFreq := l.estimate(victim.hash) if candidateFreq > victimFreq { return l.slru.write(candidate) } return candidate }
TinyLFU訪問
TinyLFU的訪問很簡單,只是根據(jù)entry所在的緩存段分別調(diào)用對應(yīng)的access函數(shù)實(shí)現(xiàn)訪問
func (l *tinyLFU) access(en *entry) { l.increase(en.hash) if en.listID == admissionWindow { l.lru.access(en) } else { l.slru.access(en) } }
增加entry的訪問次數(shù)
write函數(shù)中通過increase可以對entry的訪問次數(shù)+1,下面分析一下increase函數(shù):
func (l *tinyLFU) increase(h uint64) { if l.samples <= 0 { return } l.additions++ if l.additions >= l.samples { l.filter.reset() l.counter.reset() l.additions = 0 } if l.filter.put(h) { l.counter.add(h) } }
這里會(huì)判斷已采樣數(shù)量是否超出采樣窗口,若超出了則會(huì)重置doorkeeper和計(jì)數(shù)器.如果entry在doorkeeper中存在,那么filter.put(h)將返回true,此時(shí)會(huì)調(diào)用counter.add(h)來增加entry的訪問次數(shù).
估計(jì)entry訪問次數(shù)
write函數(shù)中通過estimate可以查詢entry的訪問次數(shù),下面分析一下estimate函數(shù):
func (l *tinyLFU) estimate(h uint64) uint8 { freq := l.counter.estimate(h) if l.filter.contains(h) { freq++ } return freq }
已經(jīng)在前面的章節(jié)中介紹過counter.estimate(),其根據(jù)hash值返回對應(yīng)元素的計(jì)數(shù)器中最小的值以減少哈希碰撞的影響.這里需要注意的是l.filter.contains(h),它將在doorkeeper中查找該hash值是否存在,若存在需要將估計(jì)值+1(因?yàn)閐oorkeeper中的值也算訪問次數(shù))
總結(jié)
Mango Cache中還有統(tǒng)計(jì)模塊來記錄緩存的各方面運(yùn)行狀態(tài)(命中率,緩存驅(qū)逐等等),由于不是核心內(nèi)容,因此就不在本文進(jìn)行贅述了.
Mango Cache最值得學(xué)習(xí)的點(diǎn)就是事件處理機(jī)制和TinyLFU緩存管理策略,希望讀者可以好好體會(huì)在并發(fā)狀態(tài)下,如何實(shí)現(xiàn)安全且高效的緩存操作并借助TinyLFU實(shí)現(xiàn)較高的緩存命中率.
以上就是Mango Cache緩存管理庫TinyLFU源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Mango Cache TinyLFU的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法
本文主要介紹了go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Go?gRPC服務(wù)進(jìn)階middleware使用教程
這篇文章主要為大家介紹了Go?gRPC服務(wù)進(jìn)階middleware的使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06go語言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等
這篇文章主要為大家介紹了go語言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例
這篇文章主要為大家詳細(xì)介紹了Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09GoLang并發(fā)編程中條件變量sync.Cond的使用
Go標(biāo)準(zhǔn)庫提供Cond原語的目的是,為等待/通知場景下的并發(fā)問題提供支持,本文主要介紹了Go并發(fā)編程sync.Cond的具體使用,具有一定的參考價(jià)值,感興趣的可以了解一下2023-01-01