欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Mango?Cache緩存管理庫TinyLFU源碼解析

 更新時(shí)間:2022年09月08日 10:08:40   作者:司司sama  
這篇文章主要為大家介紹了Mango?Cache緩存管理庫TinyLFU源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

介紹

據(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語言字符串常用操作小結(jié)

    GO語言字符串常用操作小結(jié)

    本文主要介紹了GO語言字符串常用操作小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法

    go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法

    本文主要介紹了go程序部署到linux上運(yùn)行的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • Go?gRPC服務(wù)進(jìn)階middleware使用教程

    Go?gRPC服務(wù)進(jìn)階middleware使用教程

    這篇文章主要為大家介紹了Go?gRPC服務(wù)進(jìn)階middleware的使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • go語言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等

    go語言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等

    這篇文章主要為大家介紹了go語言題解LeetCode453最小操作次數(shù)使數(shù)組元素相等示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例

    Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例

    這篇文章主要為大家詳細(xì)介紹了Go調(diào)用opencv實(shí)現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-09-09
  • Go如何優(yōu)雅的使用字節(jié)池示例詳解

    Go如何優(yōu)雅的使用字節(jié)池示例詳解

    在編程開發(fā)中,我們經(jīng)常會(huì)需要頻繁創(chuàng)建和銷毀同類對象的情形,這樣的操作很可能會(huì)對性能造成影響,這時(shí)常用的優(yōu)化手段就是使用對象池(object pool),這篇文章主要給大家介紹了關(guān)于Go如何優(yōu)雅的使用字節(jié)池的相關(guān)資料,需要的朋友可以參考下
    2022-08-08
  • GoLang并發(fā)編程中條件變量sync.Cond的使用

    GoLang并發(fā)編程中條件變量sync.Cond的使用

    Go標(biāo)準(zhǔn)庫提供Cond原語的目的是,為等待/通知場景下的并發(fā)問題提供支持,本文主要介紹了Go并發(fā)編程sync.Cond的具體使用,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-01-01
  • GO語言數(shù)組和切片實(shí)例詳解

    GO語言數(shù)組和切片實(shí)例詳解

    這篇文章主要介紹了GO語言數(shù)組和切片的用法,以實(shí)例形式較為詳細(xì)的分析了GO語言中數(shù)組與切片的創(chuàng)建及使用技巧,是深入學(xué)習(xí)GO語言的基礎(chǔ),需要的朋友可以參考下
    2014-12-12
  • Go語言Gin框架獲取請求參數(shù)的兩種方式

    Go語言Gin框架獲取請求參數(shù)的兩種方式

    在添加路由處理函數(shù)之后,就可以在路由處理函數(shù)中編寫業(yè)務(wù)處理代碼了,而編寫業(yè)務(wù)代碼第一件事一般就是獲取HTTP請求的參數(shù)吧,Gin框架在net/http包的基礎(chǔ)上封裝了獲取參數(shù)的方式,本文小編給大家介紹了獲取參數(shù)的兩種方式,需要的朋友可以參考下
    2024-01-01
  • Go語言中切片使用的注意事項(xiàng)小結(jié)

    Go語言中切片使用的注意事項(xiàng)小結(jié)

    切片是引用類型,相信對大家來說都不陌生,下面這篇文章主要給大家總結(jié)介紹了關(guān)于Go語言中切片使用的一些注意事項(xiàng),文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-01-01

最新評(píng)論