Реализуйте свой собственный потокобезопасный кеш без внешних зависимостей

Кэширование является важным аспектом оптимизации производительности современных приложений. Он позволяет хранить и быстро извлекать результаты дорогостоящих операций или часто используемых данных, уменьшая необходимость повторного вычисления или многократного извлечения данных. В этой статье мы рассмотрим, как реализовать потокобезопасный кеш в Go с помощью пакета sync.Map. Эта реализация кэша поддерживает истечение срока действия записей кэша, гарантируя, что устаревшие данные не останутся в кэше.

Зачем вообще заморачиваться

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

Реализация собственного потокобезопасного кеша с помощью sync.Map Go может иметь несколько преимуществ по сравнению с использованием внешних библиотек, таких как Redis, в зависимости от вашего варианта использования и требований. Вот несколько причин, по которым создание собственного кеша с помощью sync.Map может быть выгодным:

  1. Меньшая задержка: при использовании кэша в памяти, такого как реализованный в sync.Map, данные хранятся в памяти вашего приложения. Это может привести к меньшей задержке доступа по сравнению с отдельной службой, такой как Redis, которая требует сетевого взаимодействия между вашим приложением и службой кэширования.
  2. Более простое развертывание: с кэшем на основе sync.Map нет необходимости развертывать, настраивать и поддерживать дополнительные службы, такие как Redis. Ваше решение для кэширования является частью вашего приложения, что упрощает процесс развертывания и потенциально снижает эксплуатационную сложность.
  3. Сокращение использования ресурсов: Кэш в памяти с sync.Map обычно потребляет меньше ресурсов, чем внешняя служба, такая как Redis, которая может экономить память и использование ЦП. Это может быть более рентабельным, особенно для приложений меньшего масштаба или приложений с жесткими ограничениями ресурсов.
  4. Простая интеграция: реализация кэша с использованием sync.Map непосредственно в вашем приложении Go может упростить интеграцию с существующей кодовой базой. Вам не нужно изучать новый API или управлять подключениями к внешней службе.
  5. Настройка: при создании собственной реализации кэша вы полностью контролируете его поведение и функции. Вы можете легко адаптировать кеш к своим конкретным потребностям, оптимизировать его для своего варианта использования и добавить настраиваемые политики истечения срока действия или другие функции по мере необходимости.
  6. Развлечение: создание собственного фрагмента кода, реализующего кеширование, доставляет массу удовольствия и помогает лучше понять внешние библиотеки, обеспечивающие функциональность кеширования. И их лучшее понимание помогает лучше использовать все функциональные возможности, которые они предоставляют.

Однако важно отметить, что использование внешнего решения для кэширования, такого как Redis, имеет свои преимущества, особенно для крупномасштабных приложений или приложений с более сложными требованиями к кэшированию. Некоторые преимущества использования Redis включают в себя:

  1. Масштабируемость: Redis разработан для обеспечения высокой производительности и может масштабироваться горизонтально для обработки большого количества запросов и объемов данных.
  2. Постоянство: Redis поддерживает различные уровни постоянства данных, гарантируя, что данные вашего кэша выживут при перезапуске или сбое.
  3. Расширенные функции: Redis предлагает широкий спектр функций, помимо простого кэширования значений ключа, таких как структуры данных, обмен сообщениями pub/sub и многое другое.

В конечном счете, выбор между реализацией собственного кэша с помощью sync.Map или использованием внешней библиотеки, такой как Redis, будет зависеть от ваших конкретных потребностей, масштаба вашего приложения и компромиссов, на которые вы готовы пойти с точки зрения производительности, сложности и производительности. Ресурсы.

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

Почему мы используем sync.Map

Просто потому, что он идеально подходит для наших нужд. Более глубокое объяснение — sync.Map — это параллельная, потокобезопасная реализация карты в стандартной библиотеке Go. Он предназначен для использования в случаях, когда к карте одновременно обращаются несколько горутин, а количество ключей неизвестно или меняется со временем.

Важно отметить, что, хотя sync.Map — отличный выбор для конкретных случаев использования, он не предназначен для замены встроенного типа map во всех сценариях. В частности, sync.Map лучше всего подходит для случаев, когда:

  • Карта в основном предназначена для чтения, с редкими записями.
  • Количество ключей меняется со временем или заранее неизвестно.
  • К карте одновременно обращаются несколько горутин.

В случаях, когда количество ключей фиксировано или известно заранее, а карта может быть предварительно выделена, встроенный тип map с соответствующей синхронизацией с использованием sync.Mutex или sync.RWMutex может обеспечить лучшую производительность.

Создание SafeCache

Наш SafeCache, как упоминалось выше, представляет собой простой потокобезопасный кеш, который использует sync.Map Go для хранения своих пар ключ-значение.

Во-первых, мы определяем структуру CacheEntry для хранения значения и отметки времени его истечения:

type CacheEntry struct {
	value      interface{}
	expiration int64
}

Структура SafeCache включает структуру sync.Map, которая обеспечивает безопасный параллелизм доступ к парам ключ-значение:

type SafeCache struct {
	syncMap sync.Map
}

Добавление значений в кэш

Затем мы определяем метод Set, позволяющий нам хранить значение в кеше с указанным временем жизни (TTL). TTL определяет, как долго запись кэша должна считаться действительной. По истечении TTL запись в кэше удаляется во время следующего цикла очистки:

func (sc *SafeCache) Set(key string, value interface{}, ttl time.Duration) {
	expiration := time.Now().Add(ttl).UnixNano()
	sc.syncMap.Store(key, CacheEntry{value: value, expiration: expiration})
}

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

Следующий необходимый метод — это Get, который извлекает значение из кеша, используя свой ключ. Если значение не найдено или срок его действия истек, метод возвращает false:

func (sc *SafeCache) Get(key string) (interface{}, bool) {
	// ... (see the provided code for the full implementation)
}

Что важно в методе Get, так это утверждение типа после загрузки значения из кеша. Мы полагаемся на метод sync.Map Load, возвращающий интерфейс.

entry, found := sc.syncMap.Load(key)
 if !found {
  return nil, false
 }
 // Type assertion to CacheEntry, as entry is an interface{}
 cacheEntry := entry.(CacheEntry)

Удаление значений из кэша

И, конечно же, нам нужен метод Delete, позволяющий удалить значение из кеша:

func (sc *SafeCache) Delete(key string) {
	sc.syncMap.Delete(key)
}

Очистка просроченных записей

Мы расширяем кеш методом CleanUp, который отвечает за периодическое удаление просроченных записей из кеша. Он использует метод Range, предоставленный sync.Map, для перебора всех пар ключ-значение в кеше и удаления пар с просроченным TTL:

func (sc *SafeCache) CleanUp() {
	// ... (see the provided code for the full implementation)
}

Чтобы запустить метод CleanUp, мы можем запустить отдельную горутину при инициализации кеша:

cache := &SafeCache{}
go cache.CleanUp()

И весь фрагмент кода

package cache

import (
 "sync"
 "time"
)

// CacheEntry is a value stored in the cache.
type CacheEntry struct {
 value      interface{}
 expiration int64
}

// SafeCache is a thread-safe cache.
type SafeCache struct {
 syncMap sync.Map
}

// Set stores a value in the cache with a given TTL
// (time to live) in seconds.
func (sc *SafeCache) Set(key string, value interface{}, ttl time.Duration) {
 expiration := time.Now().Add(ttl).UnixNano()
 sc.syncMap.Store(key, CacheEntry{value: value, expiration: expiration})
}

// Get retrieves a value from the cache. If the value is not found
// or has expired, it returns false.
func (sc *SafeCache) Get(key string) (interface{}, bool) {
 entry, found := sc.syncMap.Load(key)
 if !found {
  return nil, false
 }
 // Type assertion to CacheEntry, as entry is an interface{}
 cacheEntry := entry.(CacheEntry)
 if time.Now().UnixNano() > cacheEntry.expiration {
  sc.syncMap.Delete(key)
  return nil, false
 }
 return cacheEntry.value, true
}

// Delete removes a value from the cache.
func (sc *SafeCache) Delete(key string) {
 sc.syncMap.Delete(key)
}

// CleanUp periodically removes expired entries from the cache.
func (sc *SafeCache) CleanUp() {
 for {
  time.Sleep(1 * time.Minute)
  sc.syncMap.Range(func(key, entry interface{}) bool {
   cacheEntry := entry.(CacheEntry)
   if time.Now().UnixNano() > cacheEntry.expiration {
    sc.syncMap.Delete(key)
   }
   return true
  })
 }
}

Наконец, вы можете запустить ниже программу main.go, чтобы проверить работу кеша. Мы создаем HTTP-сервер, который прослушивает запросы в конечной точке «/compute». Сервер принимает целое число n в качестве параметра запроса и возвращает результат дорогостоящих вычислений (в данном случае — простую квадратную операцию с симулированной задержкой). Сначала сервер проверяет кеш, чтобы убедиться, что результат для данного ввода уже закэширован; если нет, он вычисляет результат, сохраняет его в кэше и возвращает клиенту.

Чтобы протестировать сервер, запустите код и сделайте запрос на http://localhost:8080/compute?n=5. Первый запрос займет больше времени (из-за симулированной задержки), но последующие запросы с тем же n вернут кэшированный результат мгновенно.

package main

import (
 "fmt"
 "log"
 "net/http"
 "safe-cache/cache"
 "strconv"
 "time"
)


func expensiveComputation(n int) int {
 // Simulate an expensive computation
 time.Sleep(2 * time.Second)
 return n * n
}

func main() {
 safeCache := &cache.SafeCache{}
 // Start a goroutine to periodically clean up the cache
 go safeCache.CleanUp()

 http.HandleFunc("/compute", func(w http.ResponseWriter, r *http.Request) {
  query := r.URL.Query()
  n, err := strconv.Atoi(query.Get("n"))
  if err != nil {
   http.Error(w, "Invalid input", http.StatusBadRequest)
   return
  }

  cacheKey := fmt.Sprintf("result_%d", n)
  cachedResult, found := safeCache.Get(cacheKey)
  var result int
  if found {
   result = cachedResult.(int)
  } else {
   result = expensiveComputation(n)
   safeCache.Set(cacheKey, result, 1*time.Minute)
  }

  _, err = fmt.Fprintf(w, "Result: %d\n", result)
  if err != nil {
   return
  }
 })

 log.Fatal(http.ListenAndServe(":8080", nil))
}

Заключение

В этой статье мы продемонстрировали, как реализовать простой потокобезопасный кеш в Go с помощью пакета sync.Map. Эта реализация кэша поддерживает хранилище ключей и значений с истечением срока действия на основе TTL и может быть легко интегрирована в ваши приложения Go для повышения производительности и снижения нагрузки на ваши источники данных или вычислительные ресурсы.

Если вам нравится читать статьи на Medium и вы заинтересованы в том, чтобы стать участником, я буду рад поделиться с вами своей реферальной ссылкой!

https://medium.com/@adamszpilewicz/membership

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу