Имплементация LRU кэша на Go

LRU: Least Recently Used — алгоритм кэширования, при котором вытесняются значения, которые дольше всего не запрашивались. Алгоритмическая сложность O(1), а потому кеш работает очень быстро и используется в memcached.

Кеш имеет очередь фиксированного размера. Когда новый элемент попадает в кеш, то добавляется в начало очереди. При запросе элемента очередь выталкивает элемент в начало, а если нужно освободить место, то из вытесняется последний элемент.

Свой memcached на Go

Писать будем потоко-небезопасную реализацию LRU кэша с фиксированным количеством ячеек. В основе алгоритма двусвязный список и хеш-таблица. Двусвязный список будет очередью, а в хеш-таблицу запишем соответствия ключа и ссылки на значение.

import (
    "container/list"
)
type Item struct {
    Key   string
    Value interface{}
}
type LRU struct {
    capacity int
    items    map[string]*list.Element
    queue    *list.List
}
func NewLru(capacity int) *LRU {
    return &LRU{
        capacity: capacity,
        items:    make(map[string]*list.Element),
        queue:    list.New(),
    }
}

Структура LRU содержит поле с количеством ячеек, поле двусвязного списка *list.List и поле для хранения хеш-таблицы map[string]*list.Element. А Item содержит поля для хранения ключа и значения кэшируемого элемента. Функция конструктор NewLru инициализирует LRU и возвращает ссылку на экземпляр.

Сохраняем значение в кэше

При сохранении элемента в кеше, инициализируем новую структуру Item и добавляем ее в начало очереди c.queue.PushFront(item). Возвращенный очередью *Element добавляем в хеш-таблицу, где ключ это идентификатор записи, а значение это ссылка на элемент очереди.

func (c *LRU) Set(key string, value interface{}) bool {
    if element, exists := c.items[key]; exists == true {
        c.queue.MoveToFront(element)
        element.Value.(*Item).Value = value
        return true
    }
    if c.queue.Len() == c.capacity {
        c.purge()
    }
    item := &Item{
        Key:   key,
        Value: value,
    }
    element := c.queue.PushFront(item)
    c.items[item.Key] = element
    return true
}

Перед добавлением в очередь проверяем нет ли уже такого ключа в хеш-таблице и если есть, то заменяем значение на новое и двигаем в начало очереди c.queue.MoveToFront(element).

Если количество элементов очереди равно максимальному количеству ячеек, то пора выбросить последний элемент из очереди и ключ из хеш-таблицы вызвав функцию purge().

func (c *LRU) purge() {
    if element := c.queue.Back(); element != nil {
        item := c.queue.Remove(element).(*Item)
        delete(c.items, item.Key)
    }
}

Получаем значение из кэша

При запросе элемента из кеша, ищем соответствие ключа в хеш-таблице и при нахождении получаем значение элемента через каст значения на структуру element.Value.(*Item).Value. Перед возвращением перемещаем элемент в начало очереди.

func (c *LRU) Get(key string) interface{} {
    element, exists := c.items[key]
    if exists == false {
        return nil
    }
    c.queue.MoveToFront(element)
    return element.Value.(*Item).Value
}

Дальше можно добавить mutex и сделать функцию потоко-безопасной. А еще можно заменить количество ячеек на размер кеша по объему памяти хранимых сущностей.