首頁 > 後端開發 > Golang > 主體

在 Go 中實現 LRU 緩存

WBOY
發布: 2024-08-05 16:04:32
原創
893 人瀏覽過

Implement an LRU Cache in Go

因此您需要一個小型緩存,並且無法證明 Redis 或 memcached 實例是合理的。讓我們看看如何在 Go 中實現一個。為了好玩,我們將使用泛型來製作它,這樣它就可以在我們的專案中重複使用。

LRU 快取通常具有固定的容量和最簡單的彈出策略:彈出存取時間最長的元素。一個簡單的 lru 快取將實現以下介面:

type LRUCache[T any] interface {
    Get(key string) (value T, found bool)
    Put(key string, value T)
    Keys() []string
    Remove(key string) bool
    Clear()
    Capacity() int
    Len() int
}
登入後複製

我們知道快取會將資料項目儲存為由某個值作為鍵的條目。這聽起來像是一張地圖。執行驅逐政策又如何呢?實現此目的的一種方法是為每個項目保留 timeAccessed 屬性。類似:

type cacheEntry[T any] struct {
  Data T
  LastAccessed time.time
}
登入後複製

但是,讓我們考慮一下效能,我們希望能夠搜尋快取鍵以及插入和逐出最舊的鍵(如有必要),盡可能快。

使用映射(即哈希表)將為我們提供相當快的查找效能。找到最舊的條目怎麼樣?如果您的快取結構如下所示:

type LRUCache[T any] {
  capacity int
  keyMap map[string]cacheEntry[T]
}
登入後複製

當需要驅逐條目時,我們必然需要迭代地圖以找到最舊的條目。

我們需要一種儲存條目的方法,使我們能夠有效地保持快取條目清單的排序。最好我們不需要使用排序例程。

雙鍊錶是實現此目的的好方法,除非我們確實需要,否則我們不需要在條目中儲存存取時間。因此,假設我們有一個鍊錶,它實現了以下內容及其節點結構:

type DoubleLinkedList[T any] interface {
    Head() *DoubleNode[T]
    Tail() *DoubleNode[T]
    // Append inserts new item at tail
    Append(data T) *DoubleNode[T]
    // Push appends new item at head
    Push(data T) *DoubleNode[T]
    Remove(node *DoubleNode[T]) *DoubleNode[T]
    RemoveTail() *DoubleNode[T]
    MoveToHead(node *DoubleNode[T])
}
type DoubleNode[T any] struct {
    Data T
    Prev *DoubleNode[T]
    Next *DoubleNode[T]
}
登入後複製

快取結構現在看起來像:

type lruCache[T any] struct {
    capacity int
    keyMap   map[string]*DoubleNode[lruCacheEntry[T]]
    list     DoubleLinkedList[lruCacheEntry[T]]
}
登入後複製

快取條目結構將是:

type lruCacheEntry[T any] struct {
    key   string
    value T
}
登入後複製

實際上,您可能會使用快取鍵的介面。我使用字串來保持程式碼簡單。

在這裡的實作中,快取中最近存取的條目將位於頭部,最近最少使用的條目將位於尾部。因此,當需要驅逐時,我們只需刪除鍊錶的尾部元素即可。

實作 Get() 函數很簡單:

func (l *lruCache[T]) Get(key string) (value T, found bool) {
    if node, ok := l.keyMap[key]; ok {
        l.list.MoveToHead(node)
        return node.Data.value, ok
    }
    var zero T
    return zero, false
}
登入後複製

Get 只需檢索鍵的映射條目,然後將節點移動到清單的頭部,因為它現在是「最近使用的」。

Put() 函數是我們在必要時處理驅逐的地方:

func (l *lruCache[T]) Put(key string, value T) {
    if node, ok := l.keyMap[key]; ok {
        node.Data = lruCacheEntry[T]{
            key:   key,
            value: value,
        }
        // move the element to the most recent position
        l.list.MoveToHead(node)
    } else {
        // insert the new element at the head
        newNode := l.list.Push(lruCacheEntry[T]{
            key:   key,
            value: value,
        })
        l.keyMap[key] = newNode
    }
    // is eviction necessary
    if len(l.keyMap) > l.capacity {
        nodeRemoved := l.list.RemoveTail()
        delete(l.keyMap, nodeRemoved.Data.key)
    }
}
登入後複製

對於 Put(),我們先檢查給定鍵是否已經有一個值。如果是,則更新該值並將該節點移至鍊錶的頭部。否則,我們會建立一個新的快取條目,將其作為頭添加到列表中,並將其添加到我們的映射中。

最後,不要忘記檢查容量。如果新條目超出了容量,我們將驅逐最舊的條目(即清單的尾部)並從映射中刪除該條目。

請注意,將密鑰儲存為快取條目的一部分允許我們快速從地圖中刪除密鑰。如果我們只將資料儲存在快取條目中,那麼我們需要迭代映射才能找到它。

此快取缺少對多執行緒應用程式至關重要的東西。沒有同步。實際上,快取將由多個執行緒存取。同步是一個複雜的話題。對於我們的實現,我們可以為快取結構添加一個互斥鎖:

type lruCache[T any] struct {
    capacity int
    keyMap   map[string]DoubleNode[lruCacheEntry[T]]
    list     DoubleLinkedList[lruCacheEntry[T]]
    mutex    sync.RWMutex
}
登入後複製

然後在每個函數的開頭加入以下內容。

    l.mutex.Lock()
    defer l.mutex.Unlock()
登入後複製

請注意,我們正在使用讀/寫鎖定。有些函數不會改變快取的結構,所以我們可以使用提供的讀鎖方法,例如Len()函數:

func (l *lruCache[T]) Len() int {
    l.mutex.RLock()
    defer l.mutex.RUnlock()
    return len(l.keyMap)
}
登入後複製

請注意,如果有大量執行緒嘗試存取緩存,此處選擇的同步策略可能會崩潰。這是一個複雜的主題,本身可能是一系列貼文。

請參閱下面連結中給出的儲存庫中的完整實作。

你會採取什麼不同的措施來實現快取?您將如何解決同步問題?我很想聽聽您對此的想法。對此沒有單一的解決方案,因此請在下面發表您的評論。

謝謝!

這篇文章以及本系列所有文章的程式碼可以在這裡找到

以上是在 Go 中實現 LRU 緩存的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板