go實(shí)現(xiàn)LFU緩存淘汰算法

注:以下的緩存淘汰算法實(shí)現(xiàn),不考慮 并發(fā) 和 GC(垃圾回收) 問題

本文討論的是 進(jìn)程內(nèi)緩存,是存放在內(nèi)存中的,因此容量有限。當(dāng)緩存容量達(dá)到某個閾值時,應(yīng)該刪除一條或多條數(shù)據(jù)。至于移出哪些數(shù)據(jù)?答案是移出那些 "無用" 的數(shù)據(jù)。而如何判斷數(shù)據(jù)是否 "無用",就設(shè)計到 緩存淘汰算法

常見的緩存淘汰算法有以下三種:

  1. FIFO(first in first )先進(jìn)先出算法
    《go實(shí)現(xiàn)FIFO緩存淘汰算法》
  2. LFU(least frequently used)最少使用算法
    看本文
  3. LRU(least recently used)最近最少使用算法
    《go實(shí)現(xiàn)LRU緩存淘汰算法》


LFU(最少使用)算法會淘汰緩存中訪問次數(shù)最少的數(shù)據(jù)。
其核心原則是,如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也會更高。
在 LFU 實(shí)現(xiàn)中,需要維護(hù)一個按照訪問次數(shù)排序的隊列,每次訪問時,次數(shù)加1,隊伍重新排序,淘汰時選擇訪問次數(shù)最少的即可

結(jié)構(gòu)圖如下所示:

ps:想詳細(xì)了解堆數(shù)據(jù)結(jié)構(gòu)的可以看《二叉堆》

如上圖示
'queue'是一個二叉堆實(shí)現(xiàn)的隊列
'weight'為訪問次數(shù)

相對FIFO算法,LFU 使用了 堆queue,而不是 雙鏈表。
二叉堆 插入記錄,更新記錄,刪除記錄,時間復(fù)雜度都是 'O(logN)'
map 用來存儲鍵值對,每次訪問鍵值時,都必須更新weight,因此獲取記錄時間復(fù)雜度也是 'O(logN)'
如下,LFU算法 的代碼實(shí)現(xiàn)如下
// 最小堆實(shí)現(xiàn)的隊列
type queue []*entry

// 隊列長度
func (q queue) Len() int {
    return len(q)
}

// '<' 是最小堆,'>' 是最大堆
func (q queue) Less(i, j int) bool {
    return q[i].weight < q[j].weight
}

// 交換元素
func (q queue) Swap(i, j int) {
    // 交換元素
    q[i], q[j] = q[j], q[i]
    // 索引不用交換
    q[i].index = i
    q[j].index = j
}

// append ,*q = oldQue[:n-1] 會導(dǎo)致頻繁的內(nèi)存拷貝
// 實(shí)際上,如果使用 LFU算法,處于性能考慮,可以將最大內(nèi)存限制修改為最大記錄數(shù)限制
// 這樣提前分配好 queue 的容量,再使用交換索引和限制索引的方式來實(shí)現(xiàn) Pop 方法,可以免去頻繁的內(nèi)存拷貝,極大提高性能
func (q *queue) Push(v interface{}) {
    n := q.Len()
    en := v.(*entry)
    en.index = n
    *q = append(*q, en) // 這里會重新分配內(nèi)存,并拷貝數(shù)據(jù)
}

func (q *queue) Pop() interface{} {
    oldQue := *q
    n := len(oldQue)
    en := oldQue[n-1]
    oldQue[n-1] = nil // 將不再使用的對象置為nil,加快垃圾回收,避免內(nèi)存泄漏
    *q = oldQue[:n-1] // 這里會重新分配內(nèi)存,并拷貝數(shù)據(jù)
    return en
}

// weight更新后,要重新排序,時間復(fù)雜度為 O(logN)
func (q *queue) update(en *entry, val interface{}, weight int) {
    en.value = val
    en.weight = weight
    (*q)[en.index] = en
    // 重新排序
    // 分析思路是把 堆(大D) 的樹狀圖畫出來,看成一個一個小的堆(小D),看改變其中一個值,對 大D 有什么影響
    // 可以得出結(jié)論,下沉操作和上沉操作分別執(zhí)行一次能將 queue 排列為堆
    heap.Fix(q, en.index)
}
// 定義cache接口
type Cache interface {
    // 設(shè)置/添加一個緩存,如果key存在,則用新值覆蓋舊值
    Set(key string, value interface{})
    // 通過key獲取一個緩存值
    Get(key string) interface{}
    // 通過key刪除一個緩存值
    Del(key string)
    // 刪除 '最無用' 的一個緩存值
    DelOldest()
    // 獲取緩存已存在的元素個數(shù)
    Len() int
    // 緩存中 元素 已經(jīng)所占用內(nèi)存的大小
    UseBytes() int
}

// 結(jié)構(gòu)體,數(shù)組,切片,map,要求實(shí)現(xiàn) Value 接口,該接口只有1個 Len 方法,返回占用內(nèi)存的字節(jié)數(shù)
type Value interface {
    Len() int
}

// 定義元素
type entry struct {
    key    string
    value  interface{}
    weight int // 訪問次數(shù)
    index  int // queue索引
}

// 計算出元素占用內(nèi)存字節(jié)數(shù)
func (e *entry) Len() int {
    return CalcLen(e.value)
}

// 計算value占用內(nèi)存大小
func CalcLen(value interface{}) int {
    var n int
    switch v := value.(type) {
    case Value: // 結(jié)構(gòu)體,數(shù)組,切片,map,要求實(shí)現(xiàn) Value 接口,該接口只有1個 Len 方法,返回占用的內(nèi)存字節(jié)數(shù),如果沒有實(shí)現(xiàn)該接口,則panic
        n = v.Len()
    case string:
        if runtime.GOARCH == "amd64" {
            n = 16 + len(v)
        } else {
            n = 8 + len(v)
        }
    case bool, int8, uint8:
        n = 1
    case int16, uint16:
        n = 2
    case int32, uint32, float32:
        n = 4
    case int64, uint64, float64:
        n = 8
    case int, uint:
        if runtime.GOARCH == "amd64" {
            n = 8
        } else {
            n = 4
        }
    case complex64:
        n = 8
    case complex128:
        n = 16
    default:
        panic(fmt.Sprintf("%T is not implement cache.value", value))
    }

    return n
}

// 定義lfu cache 結(jié)構(gòu)體
type lfu struct {
    // 緩存最大容量,單位字節(jié),這里值最大存放的 元素 的容量,key不算
    maxBytes int

    // 已使用的字節(jié)數(shù),只包括value, key不算
    usedBytes int

    // 最小堆實(shí)現(xiàn)的隊列
    queue *queue
    // map的key是字符串,value是entry
    cache map[string]*entry
}

// 創(chuàng)建一個新 Cache,如果 maxBytes 是0,則表示沒有容量限制
func NewLfuCache(maxBytes int) Cache {
    queue := make(queue, 0)
    return &lfu{
        maxBytes: maxBytes,
        queue:    &queue,
        cache:    make(map[string]*entry),
    }
}

// 通過 Set 方法往 Cache 頭部增加一個元素,如果存在則更新值
func (l *lfu) Set(key string, value interface{}) {
    if en, ok := l.cache[key]; ok {
        l.usedBytes = l.usedBytes - en.Len() + CalcLen(value) // 更新占用內(nèi)存長度
        l.queue.update(en, value, en.weight+1)
    } else {
        en := &entry{
            key:   key,
            value: value,
        }

        heap.Push(l.queue, en)  // 插入queue 并重新排序為堆
        l.cache[key] = en       // 插入 map
        l.usedBytes += en.Len() // 更新內(nèi)存占用

        // 如果超出內(nèi)存長度,則刪除最 '無用' 的元素,0表示無內(nèi)存限制
        for l.maxBytes > 0 && l.usedBytes >= l.maxBytes {
            l.DelOldest()
        }
    }
}

// 獲取指定元素,訪問次數(shù)加1
func (l *lfu) Get(key string) interface{} {
    if en, ok := l.cache[key]; ok {
        l.queue.update(en, en.value, en.weight+1)
        return en.value
    }
    return nil
}

// 刪除指定元素(刪除queue和map中的val)
func (l *lfu) Del(key string) {
    if en, ok := l.cache[key]; ok {
        heap.Remove(l.queue, en.index)
        l.removeElement(en)
    }
}

// 刪除最 '無用' 元素(刪除queue和map中的val)
func (l *lfu) DelOldest() {
    if l.Len() == 0 {
        return
    }
    val := heap.Pop(l.queue)
    l.removeElement(val)
}

// 刪除元素并更新內(nèi)存占用大小
func (l *lfu) removeElement(v interface{}) {
    if v == nil {
        return
    }

    en := v.(*entry)

    delete(l.cache, en.key)
    l.usedBytes -= en.Len()
}

// 緩存池元素個數(shù)
func (l *lfu) Len() int {
    return l.queue.Len()
}

// 緩存池占用內(nèi)存大小
func (l *lfu) UseBytes() int {
    return l.usedBytes
}

測試:
func TestLfuCache(t *testing.T) {
    cache := NewLfuCache(512)

    for i := 0; i < 10; i++ {
        key := fmt.Sprintf("key-%d", i)
        cache.Set(key, i)
    }
    fmt.Printf("cache 元素個數(shù):%d, 占用內(nèi)存 %d 字節(jié), key-9 = %v\n\n", cache.Len(), cache.UseBytes(), cache.Get("key-9"))
    for i := 0; i < 3; i++ {
        key := fmt.Sprintf("key-%d", i)
        cache.Set(key, i)
    }
    fmt.Printf("cache 元素個數(shù):%d, 占用內(nèi)存 %d 字節(jié), key-3 = %v\n\n", cache.Len(), cache.UseBytes(), cache.Get("key-3"))
    cache.Del("key-3")
    fmt.Printf("cache 元素個數(shù):%d, 占用內(nèi)存 %d 字節(jié), key-3 = %v\n\n", cache.Len(), cache.UseBytes(), cache.Get("key-3"))
    cache.DelOldest()
    fmt.Printf("cache 元素個數(shù):%d, 占用內(nèi)存 %d 字節(jié), key-9 = %v\n\n", cache.Len(), cache.UseBytes(), cache.Get("key-9"))
}
----------------------------------------------------------------------------------------------------------
結(jié)果:
=== RUN   TestLfuCache
cache 元素個數(shù):10, 占用內(nèi)存 80 字節(jié), key-9 = 9

cache 元素個數(shù):10, 占用內(nèi)存 80 字節(jié), key-3 = 3

cache 元素個數(shù):9, 占用內(nèi)存 72 字節(jié), key-3 = <nil>

cache 元素個數(shù):8, 占用內(nèi)存 64 字節(jié), key-9 = 9

--- PASS: TestLfuCache (0.00s)

代碼下載地址:go實(shí)現(xiàn)本地緩存的3種算法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。