go實現LRU緩存淘汰算法

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

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

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

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


LRU緩存淘汰算法的實現
如上圖示,實現 lru算法 的緩存架構圖:

lru算法 是相對平衡的一種算法。
核心原則是:如果數據最近被訪問過,那么將來被訪問的概率會更高
如上圖,用雙鏈表來實現的話,如果某條數據被訪問了,則把該條數據移動到鏈表尾部,
隊尾是最少使用的元素,內存超出限制時,淘汰隊尾元素即可

1. map 用來存儲鍵值對。這是實現緩存最簡單直接的數據結構,因為它的查找記錄和增加記錄時間復雜度都是 O(1)

2. list.List 是go標準庫提供的雙鏈表。
通過這個雙鏈表存放具體的值,移動任意記錄到隊首的時間復雜度都是 O(1),
在隊首增加記錄的時間復雜度是 O(1),刪除任意一條記錄的時間復雜度是 O(1)
如下,fifo算法的代碼實現
// 定義cache接口
type Cache interface {
    // 設置/添加一個緩存,如果key存在,則用新值覆蓋舊值
    Set(key string, value interface{})
    // 通過key獲取一個緩存值
    Get(key string) interface{}
    // 通過key刪除一個緩存值
    Del(key string)
    // 刪除 '最無用' 的一個緩存值
    DelOldest()
    // 獲取緩存已存在的元素個數
    Len() int
    // 緩存中 元素 已經所占用內存的大小
    UseBytes() int
}

// 結構體,數組,切片,map,要求實現 Value 接口,該接口只有1個 Len 方法,返回占用內存的字節數
type Value interface {
    Len() int
}

// 定義key,value 結構
type entry struct {
    key   string
    value interface{}
}

// 計算出元素占用內存字節數
func (e *entry) Len() int {
    return CalcLen(e.value)
}

// 計算value占用內存大小
func CalcLen(value interface{}) int {
    var n int
    switch v := value.(type) {
    case Value: // 結構體,數組,切片,map,要求實現 Value 接口,該接口只有1個 Len 方法,返回占用的內存字節數,如果沒有實現該接口,則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
}

type lru struct {
    // 緩存最大容量,單位字節,這里值最大存放的 元素 的容量,key不算
    maxBytes int

    // 已使用的字節數,只包括value, key不算
    usedBytes int

    // 雙鏈表
    ll *list.List
    // map的key是字符串,value是雙鏈表中對應節點的指針
    cache map[string]*list.Element
}

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

// 通過 Set 方法往 Cache 頭部增加一個元素,如果已經存在,則移到頭部,并更新值
func (l *lru) Set(key string, value interface{}) {
    if element, ok := l.cache[key]; ok {
        // 移動到頭部
        l.ll.MoveToFront(element)
        eVal := element.Value.(*entry)
        // 重新計算內存占用
        l.usedBytes = l.usedBytes - CalcLen(eVal.value) + CalcLen(value)
        // 更新value
        element.Value = value
    } else {
        element := &entry{
            key:   key,
            value: value,
        }

        e := l.ll.PushFront(element) // 頭部插入一個元素并返回該元素
        l.cache[key] = e
        // 計算內存占用
        l.usedBytes += element.Len()
    }

    // 如果超出內存長度,則刪除隊首的節點. 0表示無內存限制
    for l.maxBytes > 0 && l.maxBytes < l.usedBytes {
        l.DelOldest()
    }
}

// 獲取指定元素(有訪問要將該元素移動到頭部)
func (l *lru) Get(key string) interface{} {
    if e, ok := l.cache[key]; ok {
        // 移動到頭部
        l.ll.MoveToFront(e)
        return e.Value.(*entry).value
    }

    return nil
}

// 刪除指定元素
func (l *lru) Del(key string) {
    if e, ok := l.cache[key]; ok {
        l.removeElement(e)
    }
}

// 刪除最 '無用' 元素,鏈表尾部為最無用元素
func (l *lru) DelOldest() {
    l.removeElement(l.ll.Back())
}

// 刪除元素并更新內存占用大小
func (l *lru) removeElement(e *list.Element) {
    if e == nil {
        return
    }

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

// 緩存池元素數量
func (l *lru) Len() int {
    return l.ll.Len()
}

// 緩存池已經占用的內存大小
func (l *lru) UseBytes() int {
    return l.usedBytes
}
測試:
func TestLruCache(t *testing.T) {
    cache := NewLruCache(512)

    key := "k1"
    cache.Set(key, 1)
    fmt.Printf("cache 元素個數:%d, 占用內存 %d 字節\n\n", cache.Len(), cache.UseBytes())

    val := cache.Get(key)
    fmt.Println(cmp.Equal(val, 1))
    cache.DelOldest()
    fmt.Printf("cache 元素個數:%d, 占用內存 %d 字節\n\n", cache.Len(), cache.UseBytes())
}
----------------------------------------------------------------------------------------------------------
結果:
=== RUN   TestLruCache
cache 元素個數:1, 占用內存 8 字節

true
cache 元素個數:0, 占用內存 0 字節

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