Mango?Cache緩存管理庫(kù)TinyLFU源碼解析
介紹
據(jù)官方所述,mango Cache是對(duì)Guava Cache基于go的部分實(shí)現(xiàn),同時(shí)mangoCache參考了Caffeine以及go-tinylfu.
支持以下緩存管理策略:
- LRU
- Segmented LRU(默認(rèn))
- TinyLFU(實(shí)驗(yàn)性)
本文將從源碼對(duì)其進(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 //用戶(hù)自定義的過(guò)期以及刷新時(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 //緩存容量
// 訪問(wèn)時(shí)的緩存管理策略
accessQueue policy
// 寫(xiě)入時(shí)的緩存管理策略,只有在expireAfterWrite或refreshAfterWrite被設(shè)置以后才啟用
writeQueue policy
// 用于processEntries的事件隊(duì)列
events chan entryEvent
// 記錄距離上一次寫(xiě)期間發(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ǔ)的值
// 下面的屬性只被緩存管理策略操作,所以不需要原子性的訪問(wèn)
accessList *list.Element //此entry目前所在的訪問(wèn)鏈表中的位置
writeList *list.Element //此entry目前所在的寫(xiě)入鏈表中的位置
listID uint8 //此entry目前所在的緩存段(SLRU)
}
localCache使用事件隊(duì)列來(lái)管理并發(fā)讀寫(xiě)事件,事件隊(duì)列是一個(gè)chan,其中流轉(zhuǎn)著entryEvent(即數(shù)據(jù)以及對(duì)應(yīng)的緩存事件),localCache通過(guò)processEntries協(xié)程來(lái)處理各種讀寫(xiě)事件.之后將詳細(xì)講解mango Cache是如何進(jìn)行讀寫(xiě)操作的.
localCache的write、access等操作底層是通過(guò)操作cache、accessQueue以及writeQueue從而實(shí)現(xiàn)的,而localCache還會(huì)負(fù)責(zé)清掃過(guò)期數(shù)據(jù)的工作.
前面提到了mango Cache支持諸如LRU、SLRU以及TinyLFU等緩存管理策略,其在localCache中即為accessQueue以及writeQueue,負(fù)責(zé)對(duì)緩存的淘汰準(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ù)用戶(hù)傳入的自定義載入函數(shù)和一些配置來(lái)初始化LoadingCache.配置包括注冊(cè)插入或者刪除時(shí)觸發(fā)的鉤子函數(shù)以及過(guò)期時(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ù)用戶(hù)傳入的緩存管理策略名字來(lái)初始化accessQueue然后根據(jù)是否有寫(xiě)過(guò)期和寫(xiě)刷新配置來(lái)決定是否初始化寫(xiě)入隊(duì)列.接著創(chuàng)建事件隊(duì)列并開(kāi)啟事件處理協(xié)程.到此為止,cache啟動(dòng)完成.
讀流程
LoadingCache的Get操作可以通過(guò)key獲取緩存值,其流程為:
先從主緩存中查詢(xún)entry
若未查詢(xún)到entry,則記錄miss并且調(diào)用用戶(hù)自定義的load方法加載緩存值并返回
若查詢(xún)到entry,先檢查是否過(guò)期
- 若過(guò)期且沒(méi)有設(shè)置loader則直接向事件處理協(xié)程發(fā)送eventDelete
- 若過(guò)期但設(shè)置了loader,則異步更新entry值
若沒(méi)有過(guò)期則更新訪問(wèn)時(shí)間并向事件處理協(xié)程發(fā)送eventAccess然后記錄命中
最后返回entry
func (c *localCache) Get(k Key) (Value, error) {
en := c.cache.get(k, sum(k)) //計(jì)算key的hash并查詢(xún)?cè)搆ey
if en == nil {
c.stats.RecordMisses(1)
return c.load(k)
}
// 檢查entry是否需要更新
now := currentTime()
if c.isExpired(en, now) {
if c.loader == nil { //如果沒(méi)有設(shè)置加載器則直接刪除該entry
c.sendEvent(eventDelete, en)
} else {
//對(duì)于loadingCache,我們不刪除這個(gè)entry
//而是把它暫時(shí)留在緩存中,所以用戶(hù)依舊可以讀取到舊的緩存值
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沒(méi)有在加載
if c.reloader == nil {
go c.refresh(en)
} else {
c.reload(en)
}
return true
}
return false
}
如果沒(méi)有用戶(hù)設(shè)置的重載器,就異步執(zhí)行refresh,refresh函數(shù)實(shí)際上就是對(duì)entry進(jìn)行加載.
而如果有重載器那么就同步執(zhí)行用戶(hù)自定義的reload函數(shù).
寫(xiě)流程
loadingCache的Put操作與Get操作類(lèi)似,流程如下:
先去主緩存查詢(xún)key是否存在,若查詢(xún)到對(duì)應(yīng)的entry,那么直接更新entry
若沒(méi)有查詢(xún)到對(duì)應(yīng)的entry,說(shuō)明其不存在,因此根據(jù)key,value初始化一個(gè)新entry
如果緩存容量足夠,則讓主緩存存儲(chǔ)該entry,此時(shí)會(huì)再次檢查主存中是否有該entry(解決并發(fā)問(wèn)題)
- 若cen不為空,說(shuō)明主緩存中已經(jīng)存在該entry,直接修改該entry即可
- 若cen為空,說(shuō)明主緩存中還不存在該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通過(guò)entryEvent chan以及processEntries協(xié)程來(lái)處理并發(fā)讀寫(xiě)事務(wù)
緩存事件一共四個(gè),分別為寫(xiě)入、訪問(wèn)、刪除以及關(guān)閉.每個(gè)業(yè)務(wù)協(xié)程通過(guò)向localCache的events chan發(fā)送entryEvent通知事件處理協(xié)程,進(jìn)而實(shí)現(xiàn)并發(fā)讀寫(xiě).
而processEntries協(xié)程內(nèi),會(huì)不斷從events chan內(nèi)取出entryEvent并執(zhí)行對(duì)應(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: //寫(xiě)入事務(wù)
c.write(e.entry)
c.postWriteCleanup() //清理操作
case eventAccess: //訪問(wèn)事務(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ī)制對(duì)于access、delete和write的操作類(lèi)似,因此這里只講解較為復(fù)雜的write操作:
首先通過(guò)調(diào)用底層訪問(wèn)隊(duì)列以及寫(xiě)入隊(duì)列的write方法
觸發(fā)用戶(hù)自定義的鉤子函數(shù)
如果write方法返回值不為空,說(shuō)明有entry被驅(qū)逐,
因此需要從寫(xiě)入隊(duì)列將其刪除,同時(shí)記錄驅(qū)逐并觸發(fā)用戶(hù)自定義的鉤子函數(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ì)講述底層訪問(wèn)隊(duì)列以及緩存管理是如何實(shí)現(xiàn)的.
清理工作
前面講到過(guò)每次進(jìn)行完write、access以及delete操作后會(huì)執(zhí)行清理工作.具體地,write操作會(huì)觸發(fā)postWriteCleanup而access和delete操作會(huì)觸發(fā)postReadCleanup.
postReadCleanup會(huì)根據(jù)當(dāng)前距離上一次寫(xiě)的read操作次數(shù)是否達(dá)到清理工作閾值來(lái)決定是否清理,這個(gè)閾值是64,也就是說(shuō)每隔64次read操作就會(huì)觸發(fā)一次清理工作
而postWriteCleanup將在每一次write操作之后觸發(fā)
真正的清理工作由expireEntries函數(shù)完成,它一次清理工作最多只會(huì)清理16個(gè)entry避免了對(duì)事件處理的長(zhǎng)時(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è)沒(méi)有過(guò)期的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在初始化過(guò)程中也初始化了緩存管理策略,由于localCache的writeQueue默認(rèn)使用LRU緩存淘汰策略,而accessQueue支持LRU、SLRU以及TinyLFU三種緩存淘汰策略,本節(jié)將著重講解accessQueue.
什么是LRU?
LRU是Least Recently Used的縮寫(xiě),即最近最少使用.在mango Cache中LRU依靠go SDK自帶的List(雙向鏈表)實(shí)現(xiàn),新緩存條目會(huì)被插入List頭部,如果List內(nèi)元素達(dá)到容量上限則刪除List尾部的元素,此元素也正是最近最少使用的元素.LRU可以使得主緩存內(nèi)的緩存條目永遠(yuǎn)是最近被訪問(wèn)的,不是最近訪問(wèn)的元素將被淘汰.
如果一個(gè)不是經(jīng)常使用的數(shù)據(jù),偶爾或者周期性的被使用,那么該數(shù)據(jù)會(huì)被加到LRU鏈表頭部,而這種不經(jīng)常使用的數(shù)據(jù),放在鏈表頭部,占用了空間;一直等到LRU淘汰完,才會(huì)被剔除鏈表;如果這種數(shù)據(jù)一次性過(guò)多,那么鏈表數(shù)據(jù)都是這種無(wú)用的數(shù)據(jù),從而會(huì)導(dǎo)致緩存命中率低下,影響系統(tǒng)性能.
什么是SLRU?
Segmented LRU(SLRU)是一種LRU的改進(jìn),主要把在一個(gè)時(shí)間窗口內(nèi)命中至少2次的記錄和命中1次的單獨(dú)存放,這樣就可以把短期內(nèi)較頻繁的緩存元素區(qū)分開(kāi)來(lái).具體做法上,SLRU包含2個(gè)固定尺寸的LRU,一個(gè)叫Probation段(A1),一個(gè)叫Protection段(A2).新記錄總是插入到A1中,當(dāng)A1的記錄被再次訪問(wèn),就把它移到A2,當(dāng)A2滿(mǎn)了需要驅(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),可以在較大訪問(wèn)量的場(chǎng)景下近似的替代LFU的數(shù)據(jù)統(tǒng)計(jì)部分(meta-data),其采用類(lèi)似布隆過(guò)濾器的位計(jì)數(shù)器來(lái)實(shí)現(xiàn)對(duì)每個(gè)key的訪問(wèn)次數(shù)的粗略統(tǒng)計(jì).(由于哈希沖突的存在,位計(jì)數(shù)器的結(jié)果將偏大)
對(duì)于較為靜態(tài)的訪問(wèn)分布,各種的緩存管理策略的差異是微不足道的,而對(duì)于偏倚較大的負(fù)載場(chǎng)景,TinyLFU體現(xiàn)出了較大的優(yōu)勢(shì).即便是使用了TinyLFU準(zhǔn)入策略進(jìn)行優(yōu)化的LRU也獲得了較大的提升.
如果想要詳細(xì)了解TinyLFU請(qǐng)閱讀論文TinyLFU: A Highly Efficient Cache Admission Policy 以及一種簡(jiǎn)略實(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)也在論文中有講解,簡(jiǎn)而言之就是緩存由window Cache、doorKeeper(布隆過(guò)濾器)、counter以及SLRU組成.主緩存使用SLRU驅(qū)逐策略和TinyLFU準(zhǔn)入策略,window Cache僅使用LRU驅(qū)逐策略無(wú)準(zhǔn)入策略.
主緩存中的SLRU策略的A1和A2區(qū)域被靜態(tài)劃分開(kāi)來(lái),80%空間被分配給熱點(diǎn)元素(A2),被驅(qū)逐者則從20%的非熱項(xiàng)(A1)中選取.任何到來(lái)的元素總是允許進(jìn)入window Cache, window Cache的淘汰元素有機(jī)會(huì)進(jìn)入主緩存,如果在經(jīng)過(guò)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 //主緩存
}
接下來(lái)本文將詳細(xì)講述mango Cache中tinyLFU的具體實(shí)現(xiàn)
counter
counter是實(shí)現(xiàn)TinyLFU的重點(diǎn),對(duì)于訪問(wèn)次數(shù)的粗略統(tǒng)計(jì)就是通過(guò)此數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的.
const sketchDepth = 4 //hash次數(shù)
type countMinSketch struct {
counters []uint64
mask uint32
}
可以看到, counter由一個(gè)uint64類(lèi)型的數(shù)組和一個(gè)mask組成, 因?yàn)槲覀兪褂玫氖?次hash的4bit計(jì)數(shù)器,因此數(shù)組的每個(gè)元素實(shí)際上是4個(gè)計(jì)數(shù)器,可以通過(guò)位操作來(lái)實(shí)現(xiàn)訪問(wè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)
}
}
我們通過(guò)傳入的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值返回對(duì)應(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ù)都是對(duì)應(yīng)的位操作,有興趣的話可以進(jìn)一步研究下.
需要注意的是estimate函數(shù)用于求給定hash對(duì)應(yīng)計(jì)數(shù)器中最小計(jì)數(shù)器的值,這是因?yàn)橛泄E鲎驳那闆r,因此計(jì)數(shù)器的值始終會(huì)大于等于真實(shí)的訪問(wèn)次數(shù),因此這里采用多次hash取其中最小值的方法來(lái)減少誤差.
同時(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)是它的寫(xiě)入策略:
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已載入緩存但是還沒(méi)有注冊(cè)到SLRU管理的鏈表中
cen.listID = probationSegment
cen.accessList = l.probationLs.PushFront(cen)
} else {
l.markAccess(cen)
}
}
// probation段超出容量但list并沒(méi)有超出容量
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
}
//記錄訪問(wèn)情況
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)
}
}
這樣一來(lái),就實(shí)現(xiàn)了SLRU的緩存管理策略.
filter
filter是一個(gè)布隆過(guò)濾器,這里不展開(kāi)講解其實(shí)現(xiàn)細(xì)節(jié),只需要了解布隆過(guò)濾器可以通過(guò)key的hash來(lái)確定這個(gè)key是否在filter中,并且其只占用非常小的內(nèi)存.如果想要詳細(xì)了解布隆過(guò)濾器,可以參考這篇文章:布隆過(guò)濾器
type bloomFilter struct {
numHashes uint32 // 每個(gè)元素使用的hash數(shù)量
bitsMask uint32 // 位向量大小
bits []uint64 // 過(guò)濾器位向量
}
//根據(jù)給定的插入元素?cái)?shù)量以及假陽(yáng)性率初始化布隆過(guò)濾器
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è)組件,接下來(lái)將詳細(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 //假陽(yáng)性率
admissionRatio = 0.01 //window Cache占比
)
func (l *tinyLFU) init(c *cache, cap int) {
if cap > 0 {
// 只在容量有上限時(shí)開(kāi)啟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寫(xiě)入
TinyLFU的寫(xiě)入流程如下
首先寫(xiě)入window Cache
如果window Cache出現(xiàn)被淘汰的candidate,則將從SLRU中選取一個(gè)victim(如果SLRU未滿(mǎn),則不會(huì)產(chǎn)生victim,此時(shí)直接將candidate插入SLRU)
在計(jì)數(shù)器中查詢(xún)candidate和victim的訪問(wèn)次數(shù)
- 若candidate的訪問(wèn)次數(shù)較大,則將其插入SLRU,淘汰victim
- 否則將candidate淘汰
根據(jù)此流程,我們可以發(fā)現(xiàn)被插入SLRU的entry的訪問(wèn)次數(shù)一定是較大的,而我們通過(guò)計(jì)數(shù)器實(shí)現(xiàn)了對(duì)entry訪問(wèn)次數(shù)的保存,這樣就結(jié)合了LRU以及LFU的優(yōu)點(diǎn)并且沒(méi)有占用過(guò)多的內(nèi)存,這正是TinyLFU最大的優(yōu)勢(shì)所在.
func (l *tinyLFU) write(en *entry) *entry {
if l.lru.cap <= 0 { //若容量無(wú)限,則直接對(duì)SLRU寫(xiě)入
return l.slru.write(en)
}
l.increase(en.hash) //entry訪問(wèn)次數(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訪問(wèn)
TinyLFU的訪問(wèn)很簡(jiǎn)單,只是根據(jù)entry所在的緩存段分別調(diào)用對(duì)應(yīng)的access函數(shù)實(shí)現(xiàn)訪問(wè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的訪問(wèn)次數(shù)
write函數(shù)中通過(guò)increase可以對(duì)entry的訪問(wèn)次數(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)來(lái)增加entry的訪問(wèn)次數(shù).
估計(jì)entry訪問(wèn)次數(shù)
write函數(shù)中通過(guò)estimate可以查詢(xún)entry的訪問(wèn)次數(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é)中介紹過(guò)counter.estimate(),其根據(jù)hash值返回對(duì)應(yīng)元素的計(jì)數(shù)器中最小的值以減少哈希碰撞的影響.這里需要注意的是l.filter.contains(h),它將在doorkeeper中查找該hash值是否存在,若存在需要將估計(jì)值+1(因?yàn)閐oorkeeper中的值也算訪問(wèn)次數(shù))
總結(jié)
Mango Cache中還有統(tǒng)計(jì)模塊來(lái)記錄緩存的各方面運(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緩存管理庫(kù)TinyLFU源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Mango Cache TinyLFU的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法
本文主要介紹了go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
Go?gRPC服務(wù)進(jìn)階middleware使用教程
這篇文章主要為大家介紹了Go?gRPC服務(wù)進(jìn)階middleware的使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
go語(yǔ)言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等
這篇文章主要為大家介紹了go語(yǔ)言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例
這篇文章主要為大家詳細(xì)介紹了Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09
GoLang并發(fā)編程中條件變量sync.Cond的使用
Go標(biāo)準(zhǔn)庫(kù)提供Cond原語(yǔ)的目的是,為等待/通知場(chǎng)景下的并發(fā)問(wèn)題提供支持,本文主要介紹了Go并發(fā)編程sync.Cond的具體使用,具有一定的參考價(jià)值,感興趣的可以了解一下2023-01-01
Go語(yǔ)言Gin框架獲取請(qǐng)求參數(shù)的兩種方式
在添加路由處理函數(shù)之后,就可以在路由處理函數(shù)中編寫(xiě)業(yè)務(wù)處理代碼了,而編寫(xiě)業(yè)務(wù)代碼第一件事一般就是獲取HTTP請(qǐng)求的參數(shù)吧,Gin框架在net/http包的基礎(chǔ)上封裝了獲取參數(shù)的方式,本文小編給大家介紹了獲取參數(shù)的兩種方式,需要的朋友可以參考下2024-01-01
Go語(yǔ)言中切片使用的注意事項(xiàng)小結(jié)
切片是引用類(lèi)型,相信對(duì)大家來(lái)說(shuō)都不陌生,下面這篇文章主要給大家總結(jié)介紹了關(guān)于Go語(yǔ)言中切片使用的一些注意事項(xiàng),文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01

