go - 緩存淘汰算法

1)FIFO(先進先出)算法

核心思想:如果一個數據最先進入緩存,那么就應該最先刪掉。類似于隊列的思想。

實現:創建一個隊列(雙向鏈表),新增記錄,獲取記錄,以及當緩存滿了,自動淘汰記錄。
目錄:


image.png

cache.go : 公共的結構體,公共的方法
Cache 接口:定義一些接口方法。

package cache

import (
    "fmt"
    "runtime"
)

type Cache interface {
    Set(key string, value interface{})
    Get(key string) interface{}
    Del(key string)
    DelOldest()
    Len() int
}

type Value interface {
    Len() int
}
// 計算對應類型的字節數
func CalcLen(value interface{}) int {
    var n int
    switch v := value.(type) {
    case Value:
        n = v.Len()
    case string:
        if runtime.GOARCH == "amd64" {
            n = 16 + len(v)
        } else {
            n = 8 + len(v)
        }
    case bool, uint8, int8:
        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 implenment cache,.value", value))

    }
    return n
}

fifo.go : FIFO算法的實現。

package fifo

import (
    "cache"
    "container/list"
)

type fifo struct {
    maxBytes  int
    onEvicted func(key string, value interface{})
    usedBytes int
    ll        *list.List
    cache     map[string]*list.Element
}
type entry struct {
    key   string
    value interface{}
}

func (e *entry) Len() int {
    return cache.CalcLen(e.value)
}

// New 創建一個新的 Cache,如果 maxBytes 是 0,表示沒有容量限制
func New(maxBytes int, onEvicted func(key string, value interface{})) cache.Cache {
    return &fifo{
        maxBytes:  maxBytes,
        onEvicted: onEvicted,
        ll:        list.New(),
        cache:     make(map[string]*list.Element),
    }
}

// Set 往 Cache 尾部增加一個元素(如果已經存在,則放入尾部,并修改值)
func (f *fifo) Set(key string, value interface{}) {
    if e, ok := f.cache[key]; ok {
        f.ll.MoveToBack(e)
        en := e.Value.(*entry)
        f.usedBytes = f.usedBytes - cache.CalcLen(en.value) + cache.CalcLen(value)
        en.value = value
        return
    }

    en := &entry{key, value}
    e := f.ll.PushBack(en)
    f.cache[key] = e

    f.usedBytes += en.Len()
    if f.maxBytes > 0 && f.usedBytes > f.maxBytes {
        f.DelOldest()
    }
}

// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (f *fifo) Get(key string) interface{} {
    if e, ok := f.cache[key]; ok {
        // LRU 算法, 這里將最近訪問的數據向后面移動,防止被刪除。
        // f.ll.MoveToBack(e)
        return e.Value.(*entry).value
    }

    return nil
}

// Del 從 cache 中刪除 key 對應的記錄
func (f *fifo) Del(key string) {
    if e, ok := f.cache[key]; ok {
        f.removeElement(e)
    }
}

// DelOldest 從 cache 中刪除最舊的記錄
func (f *fifo) DelOldest() {
    f.removeElement(f.ll.Front())
}

// Len 返回當前 cache 中的記錄數
func (f *fifo) Len() int {
    return f.ll.Len()
}

func (f *fifo) removeElement(e *list.Element) {
    if e == nil {
        return
    }

    f.ll.Remove(e)
    en := e.Value.(*entry)
    f.usedBytes -= en.Len()
    delete(f.cache, en.key)

    if f.onEvicted != nil {
        f.onEvicted(en.key, en.value)
    }
}

2)LRU 算法 (最近最少使用)

核心思想: 如果數據被訪問過,那么將來訪問的概率會更高。這個思想和FIFO其實類似。只需要在每次訪問的數據將其移動到隊尾,這樣淘汰數據時,將不被容易被淘汰。
實現: 和FIFO基本一樣。在每次GET的時候,將訪問的數據移動到隊尾。

// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (f *fifo) Get(key string) interface{} {
    if e, ok := f.cache[key]; ok {
        // LRU 算法, 這里將最近訪問的數據向后面移動,防止被刪除。
        // f.ll.MoveToBack(e)
        return e.Value.(*entry).value
    }

    return nil
}

3)LFU 算法:最少使用。

核心思想:如果數據被訪問多次,那么以后被訪問的頻率更大。 其實就是維護一個權重,如果一個數據經常被訪問,權重越大,不容易被淘汰。

實現:使用堆。
lfu.go

package lfu

import (
    "cache"
    "container/heap"
)

type lfu struct {
    maxBytes  int
    onEvicted func(key string, value interface{})
    usedBytes int
    queue     *queue
    cache     map[string]*entry
}

// New 創建一個新的 Cache,如果 maxBytes 是 0,表示沒有容量限制
func New(maxBytes int, onEvicted func(key string, value interface{})) cache.Cache {
    q := make(queue, 0, 1024)
    return &lfu{
        maxBytes:  maxBytes,
        onEvicted: onEvicted,
        queue:     &q,
        cache:     make(map[string]*entry),
    }
}

// Set 往 Cache 增加一個元素(如果已經存在,更新值,并增加權重,重新構建堆)
func (l *lfu) Set(key string, value interface{}) {
    if e, ok := l.cache[key]; ok {
        l.usedBytes = l.usedBytes - cache.CalcLen(e.value) + cache.CalcLen(value)
        l.queue.update(e, value, e.weight+1)
        return
    }

    en := &entry{key: key, value: value}
    heap.Push(l.queue, en)
    l.cache[key] = en

    l.usedBytes += en.Len()
    if l.maxBytes > 0 && l.usedBytes > l.maxBytes {
        l.removeElement(heap.Pop(l.queue))
    }
}

// Get 從 cache 中獲取 key 對應的值,nil 表示 key 不存在
func (l *lfu) Get(key string) interface{} {
    if e, ok := l.cache[key]; ok {
        l.queue.update(e, e.value, e.weight+1)
        return e.value
    }

    return nil
}

// Del 從 cache 中刪除 key 對應的元素
func (l *lfu) Del(key string) {
    if e, ok := l.cache[key]; ok {
        heap.Remove(l.queue, e.index)
        l.removeElement(e)
    }
}

// DelOldest 從 cache 中刪除最舊的記錄
func (l *lfu) DelOldest() {
    if l.queue.Len() == 0 {
        return
    }
    l.removeElement(heap.Pop(l.queue))
}

// Len 返回當前 cache 中的記錄數
func (l *lfu) Len() int {
    return l.queue.Len()
}

func (l *lfu) removeElement(x interface{}) {
    if x == nil {
        return
    }
    en := x.(*entry)
    delete(l.cache, en.key)
    l.usedBytes -= en.Len()
    if l.onEvicted != nil {
        l.onEvicted(en.key, en.value)
    }
}

queue.go : 通過heap來實現最小堆,需要實現heap.Interface接口。

package lfu

import (
    "cache"
    "container/heap"
)

type entry struct {
    key    string
    value  interface{}
    weight int
    index  int
}

func (e *entry) Len() int {
    return cache.CalcLen(e.value) + 4 + 4
}

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
}

func (q *queue) Push(x interface{}) {
    n := len(*q)
    en := x.(*entry)
    en.index = n
    *q = append(*q, en)
}

func (q *queue) Pop() interface{} {
    old := *q
    n := len(old)
    en := old[n-1]
    old[n-1] = nil // avoid memory leak
    en.index = -1  // for safety
    *q = old[0 : n-1]
    return en
}

// update modifies the weight and value of an entry in the queue.
func (q *queue) update(en *entry, value interface{}, weight int) {
    en.value = value
    en.weight = weight
    heap.Fix(q, en.index)
}

總結:

1 FIFO算法缺點:有些數據被經常訪問,但是也會經常淘汰掉。
2 LRU算法:groupcache庫使用的LRU。
3 LFU算法:對剛加入的數據不友好,因為相比于歷史數據,其權重更低。同時更新數據,訪問數據都會去更新堆,影響性能。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容