Хабрахабр

Пишем простой менеджер кеша в памяти на Go

В таких ситуациях подойдет простой и достаточно эффективный способ без использования дополнительных инструментов — кеширование в оперативной памяти.
В этой статье я расскажу, с чего начать, чтобы самостоятельно написать менеджер кеша в памяти на Go. В процессе работы над небольшими проектами часто возникает необходимость в кешировании данных и бывает так, что нет возможности использовать Redis или Memcache.

Данная статья предназначена для начинающих разработчиков исключительно в академических целях и здесь не рассматриваются такие инструменты как Redis, Memcache и т.д
Кроме того мы не будем углубляться в проблемы выделения памяти. Внимание!

Для простоты ограничимся тремя основными методами: установка Set, получение Get и удаление Delete.

Данные будем хранить в формате ключ/значение.

Структура

Первое, что необходимо сделать, это создать структуру описывающую наш контейнер-хранилище:

type Cache struct { sync.RWMutex defaultExpiration time.Duration cleanupInterval time.Duration items map[string]Item
}

  • sync.RWMutex — для безопасного доступа к данным во время чтения/записи (подробнее о мьютексах https://gobyexample.com/mutexes),
  • defaultExpiration — продолжительность жизни кеша по-умолчанию (этот параметр можно будет переопределить для каждого элемента)
  • cleanupInterval — интервал, через который запускается механизм очистки кеша (Garbage Collector, далее GC)
  • items — элементы кеша (в формате ключ/значение)

Теперь опишем структуру для элемента:

type Item struct Created time.Time Expiration int64
}

  • Value — значение. Так как оно может быть любое (число/строка/массив и т.д) необходимо указать в качестве типа interface{},
  • Created — время создания кеша,
  • Expiration — время истечения (в UnixNano) — по нему будем проверять актуальность кеша

Инициализация хранилища

Начнем с инициализации нового контейнера-хранилища:

func New(defaultExpiration, cleanupInterval time.Duration) *Cache { // инициализируем карту(map) в паре ключ(string)/значение(Item) items := make(map[string]Item) cache := Cache{ items: items, defaultExpiration: defaultExpiration, cleanupInterval: cleanupInterval, } // Если интервал очистки больше 0, запускаем GC (удаление устаревших элементов) if cleanupInterval > 0 { cache.StartGC() // данный метод рассматривается ниже } return &cache
}

Инициализация нового экземпляра кеша принимает два аргумента: defaultExpiration и cleanupInterval

  • defaultExpiration — время жизни кеша по-умолчанию, если установлено значение меньше или равно 0 — время жизни кеша бессрочно.
  • cleanupInterval — интервал между удалением просроченного кеша. При установленном значении меньше или равно 0 — очистка и удаление просроченного кеша не происходит.

На выходе получаем контейнер со структурой Cache

Second поиск просроченных ключей будет происходить каждую секунду, что негативно скажется на производительности вашей программы. Будьте внимательны при установке этих параметров, слишком маленькие или слишком большие значения могут привести к нежелательным последствиям, например если установить cleanupInterval = 1 * time. Hour — в памяти будет накапливаться неиспользуемые элементы. И наоборот установив cleanupInterval = 168 * time.

Установка значений

После того как контейнер создан, хорошо бы иметь возможность записывать в него данные, для этого напишем реализацию метода Set

func (c *Cache) Set(key string, value interface{}, duration time.Duration) { var expiration int64 // Если продолжительность жизни равна 0 - используется значение по-умолчанию if duration == 0 { duration = c.defaultExpiration } // Устанавливаем время истечения кеша if duration > 0 { expiration = time.Now().Add(duration).UnixNano() } c.Lock() defer c.Unlock() c.items[key] = Item{ Value: value, Expiration: expiration, Created: time.Now(), } }

При этом проверка на существования ключей не происходит. Set добавляет новый элемент в кэш или заменяет существующий. В качестве аргументов принимает: ключ-идентификатор в виде строки key, значение value и продолжительность жизни кеша duration.

Получение значений

С помощью Set мы записали данные в хранилище, теперь реализуем метод для их получения Get

func (c *Cache) Get(key string) (interface{}, bool) { c.RLock() defer c.RUnlock() item, found := c.items[key] // ключ не найден if !found { return nil, false } // Проверка на установку времени истечения, в противном случае он бессрочный if item.Expiration > 0 { // Если в момент запроса кеш устарел возвращаем nil if time.Now().UnixNano() > item.Expiration { return nil, false } } return item.Value, true
}

Get возвращает значение (или nil) и второй параметр bool равный true если ключ найден и false если ключ не найден или кеш устарел.

Удаление кеша

Теперь когда у нас есть установка и получение, необходимо иметь возможность удалить кеш (если он нам больше не нужен) для этого напишем метод Delete

func (c *Cache) Delete(key string) error { c.Lock() defer c.Unlock() if _, found := c.items[key]; !found { return errors.New("Key not found") } delete(c.items, key) return nil
}

Delete удаляет элемент по ключу, если ключа не существует возвращает ошибку.

Сборка мусора

Осталось реализовать поиск просроченных ключей с последующей очисткой (GC)
Для этого напишем метод StartGC, который запускается при инициализация нового экземпляра кеша New и работает пока программа не будет завершена. У нас есть добавление, получение и удаление.

func (c *Cache) StartGC() { go c.GC()
} func (c *Cache) GC() { for { // ожидаем время установленное в cleanupInterval <-time.After(c.cleanupInterval) if c.items == nil { return } // Ищем элементы с истекшим временем жизни и удаляем из хранилища if keys := c.expiredKeys(); len(keys) != 0 { c.clearItems(keys) } } } // expiredKeys возвращает список "просроченных" ключей
func (c *Cache) expiredKeys() (keys []string) { c.RLock() defer c.RUnlock() for k, i := range c.items { if time.Now().UnixNano() > i.Expiration && i.Expiration > 0 { keys = append(keys, k) } } return
} // clearItems удаляет ключи из переданного списка, в нашем случае "просроченные"
func (c *Cache) clearItems(keys []string) { c.Lock() defer c.Unlock() for _, k := range keys { delete(c.items, k) }
}

Пример использования

import ( memorycache "github.com/maxchagin/go-memorycache-example"
) // Создаем контейнер с временем жизни по-умолчанию равным 5 минут и удалением просроченного кеша каждые 10 минут
cache := memorycache.New(5 * time.Minute, 10 * time.Minute) // Установить кеш с ключем "myKey" и временем жизни 5 минут
cache.Set("myKey", "My value", 5 * time.Minute) // Получить кеш с ключем "myKey"
i := cache.Get("myKey")

Что дальше?

Если этого мало (а в 95% случаев так и есть) в качестве следующего шага можно самостоятельно реализовать методы: Теперь у нас есть менеджер кеша с минимальным функционалом, его будет достаточно для самых простых задач.

Count — получение кол-ва элементов в кеше
GetItem — получение элемента кеша
Rename — переименования ключа
Copy — копирование элемента
Increment — инкремент
Decrement — декремент
Exist — проверка элемента на существование
Expire — проверка кеша на истечение срока жизни
FlushAll — очистка всех данных
SaveFile — сохранение данных в файл
LoadFile — загрузка данных из файла

Это далеко не полный список, но для базового функционала скорее всего хватит.

Исходники c примером на github

Если вам необходим готовый менеджер кеша в памяти рекомендую обратить внимание на следующие проекты:
Реализация go-cache от patrickmn
MemoryCache от beego

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть