"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Implemente um cache LRU em Go

Implemente um cache LRU em Go

Publicado em 2024-08-06
Navegar:344

Implement an LRU Cache in Go

Portanto, você precisa de um cache pequeno e não pode justificar uma instância Redis ou memcached. Vamos ver o que é necessário para implementar um em Go. Por diversão, faremos isso usando genéricos para que seja reutilizável em nosso projeto.

Um cache LRU geralmente tem capacidade fixa e a política de ejeção mais simples: ejeta o elemento que possui o maior tempo desde que foi acessado. Um cache lru simples implementará a seguinte interface:

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
}

Sabemos que o cache armazenará um item de dados como uma entrada que é digitada por algum valor. Isso soa como um mapa. Que tal implementar a política de ejeção? Uma maneira de fazer isso é manter uma propriedade timeAccessed junto com cada item. Algo como:

type cacheEntry[T any] struct {
  Data T
  LastAccessed time.time
}

No entanto, vamos pensar no desempenho, queremos ser capazes de procurar a chave de cache, bem como inserir e despejar a mais antiga, se necessário, o mais rápido possível.

Usar um mapa, que é uma tabela hash, nos proporcionará um desempenho bastante rápido para pesquisas. Que tal encontrar a entrada mais antiga? Se a estrutura do seu cache for semelhante a:

type LRUCache[T any] {
  capacity int
  keyMap map[string]cacheEntry[T]
}

Precisaremos necessariamente percorrer o mapa para encontrar o mais antigo quando chegar a hora de despejar uma entrada.

Precisamos de uma maneira de armazenar entradas de uma forma que nos permita manter uma lista de entradas de cache ordenada de maneira eficiente. É preferível que não precisemos usar uma rotina de classificação.

Uma lista duplamente vinculada é uma boa maneira de fazer isso e não precisamos armazenar o tempo de acesso na entrada, a menos que realmente queiramos. Então, vamos supor que temos uma lista vinculada que implementa o seguinte junto com sua estrutura de nó:

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]
}

A estrutura do cache agora pode ser semelhante a:

type lruCache[T any] struct {
    capacity int
    keyMap   map[string]*DoubleNode[lruCacheEntry[T]]
    list     DoubleLinkedList[lruCacheEntry[T]]
}

A estrutura de entrada do cache será:

type lruCacheEntry[T any] struct {
    key   string
    value T
}

Realisticamente, você provavelmente usaria uma interface para a chave de cache. Estou usando uma string para manter o código simples.

Na implementação aqui, a entrada acessada mais recentemente no cache estará no início e a menos usada recentemente estará no final. Então, quando chega a hora de despejar, simplesmente removemos o elemento final da lista vinculada.

Implementar a função Get() é simples:

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 só precisa recuperar a entrada do mapa para a chave e, em seguida, mover o nó para o topo da lista, pois agora é o 'usado mais recentemente'.

A função Put() é onde trataremos do despejo se for necessário:

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)
    }
}

Para Put(), primeiro verificamos se já existe um valor para a chave fornecida. Nesse caso, atualize o valor e mova o nó para o topo da lista. Caso contrário, criamos uma nova entrada de cache, adicionamos-a à lista como cabeçalho e adicionamos-a ao nosso mapa.

Finalmente, não se esqueça de verificar a capacidade. Se a nova entrada nos ultrapassar a capacidade, expulsamos a entrada mais antiga, que é o final da lista, e excluímos a entrada do nosso mapa.

Observe que armazenar a chave como parte da entrada do cache nos permite excluir rapidamente a chave do mapa. Se tivéssemos armazenado os dados apenas na entrada do cache, precisaríamos iterar no mapa para encontrá-los.

Este cache está faltando algo crítico para um aplicativo multithread. Não há sincronização. Realisticamente, um cache seria acessado por vários threads. A sincronização é um tópico complexo. Para nossa implementação, podemos adicionar um mutex à estrutura de cache:

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

em seguida, adicione o seguinte no início de cada função.

    l.mutex.Lock()
    defer l.mutex.Unlock()

Observe que estamos usando um bloqueio de leitura/gravação. Algumas das funções não alteram a estrutura do cache, então podemos usar o método read lock fornecido, por exemplo a função Len():

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

Observe que a estratégia de sincronização escolhida aqui pode falhar se houver um grande número de threads tentando acessar o cache. É um tópico complexo que poderia ser uma série de postagens.

Veja a implementação completa no repositório fornecido no link abaixo.

O que você faria de diferente para implementar um cache? Como você abordaria a sincronização? Estou interessado em ouvir sua opinião sobre este. Não há uma solução única para isso, então deixe seus comentários abaixo.

Obrigado!

O código desta postagem e de todas as postagens desta série pode ser encontrado aqui

Declaração de lançamento Este artigo foi reproduzido em: https://dev.to/johnscode/implement-an-lru-cache-in-go-1hbc?1 Se houver alguma violação, entre em contato com [email protected] para excluí-la
Tutorial mais recente Mais>

Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.

Copyright© 2022 湘ICP备2022001581号-3