В процессе работы над небольшими проектами часто возникает необходимость в кешировании данных и бывает так, что нет возможности использовать 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 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
Будьте внимательны при установке этих параметров, слишком маленькие или слишком большие значения могут привести к нежелательным последствиям, например если установить 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(), } }
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
