В процессе работы над небольшими проектами часто возникает необходимость в кешировании данных и бывает так, что нет возможности использовать Redis или Memcache. В таких ситуациях подойдет простой и достаточно эффективный способ без использования дополнительных инструментов — кеширование в оперативной памяти.
В этой статье я расскажу, с чего начать, чтобы самостоятельно написать менеджер кеша в памяти на Go.
Внимание! Данная статья предназначена для начинающих разработчиков исключительно в академических целях и здесь не рассматриваются такие инструменты как 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 {
Value interface{}
Created time.Time
Duration time.Duration
Expiration int64
}
Value
— значение. Так как оно может быть любое (число/строка/массив и т.д) необходимо указать в качестве типаinterface{}
,Created
— время создания кеша,Duration
— продолжительность жизни,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
Будьте внимательны при установке этих параметров, слишком маленькие или слишком большие значения могут привести к нежелательным последствиям, например если установить cleanupInterval = 1 * time.Second
поиск просроченных ключей будет происходить каждую секунду, что негативно скажется на производительности вашей программы. И наоборот установив cleanupInterval = 168 * time.Hour
— в памяти будет накапливаться неиспользуемые элементы.
Установка значений
После того как контейнер создан, хорошо бы иметь возможность записывать в него данные, для этого напишем реализацию метода 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(),
Duration: duration,
}
}
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 {
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)
}
}
Что дальше?
Теперь у нас есть менеджер кеша с минимальным функционалом, его будет достаточно для самых простых задач. Если этого мало (а в 95% случаев так и есть) в качестве следующего шага можно самостоятельно реализовать методы:
Count — получение кол-ва элементов в кеше
GetItem — получение элемента кеша
Rename — переименования ключа
Copy — копирование элемента
Increment — инкремент
Decrement — декремент
Exist — проверка элемента на существование
Expire — проверка кеша на истечение срока жизни
FlushAll — очистка всех данных
SaveFile — сохранение данных в файл
LoadFile — загрузка данных из файла
Это далеко не полный список, но для базового функционала скорее всего хватит.
Исходники c примером на github
Если вам необходим готовый менеджер кеша в памяти рекомендую обратить внимание на следующие проекты:
Реализация go-cache от patrickmn
MemoryCache от beego
Автор: Maxchagin