Кэширование данных в памяти имеет множество преимуществ:

  • Это уменьшает количество запросов, отправляемых в основную базу данных.
  • Это повышает производительность, поскольку извлечение данных из первичной базы данных происходит медленнее, чем из памяти.
  • Это уменьшает количество внешних вызовов API, если данные изначально получены из другого веб-сервиса.

Для кэширования существует несколько решений уровня предприятия, которые обычно используются, например Redis и Memcached.

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

В этой статье мы увидим, как простое приложение Go использует кэширование памяти с помощью библиотеки Gcache.

Настройка проекта

Этот пример службы REST будет получать результаты SAT для учащихся через единую конечную точку. Во-первых, нам нужно инициализировать наш проект Go, запустив:

go mod init <The name of your project>

Затем нам нужно установить две библиотеки:

  1. gCache — библиотека кэширования.
  2. Gorilla Mux, набор инструментов HTTP для Go, который мы будем использовать для уменьшения объема стандартного кода, необходимого для написания нашего REST API.
go get github.com/bluele/gcache
go get github.com/gorilla/mux

Далее давайте создадим правильную структуру каталогов проекта. Мы создадим пару внутренних пакетов — пакеты handler и store. В идеале структура должна выглядеть так:

Пакет магазина

Пакет хранилища будет содержать один файл с кодом, который будет взаимодействовать с нашей базой данных. На самом деле мы не будем использовать настоящую базу данных, такую ​​как Postgres или MySql, для этого примера, так как это не является сегодняшней темой, а вместо этого мы смоделируем ее. Давайте посмотрим на код файла store.go в пакете store и рассмотрим, что он делает:

package store

import (
 "fmt"
 "time"

 "github.com/bluele/gcache"
)

type Store struct {
 db    map[string]int
 cache gcache.Cache
 ttl   time.Duration
}

func NewStore(db map[string]int, cache gcache.Cache, ttl time.Duration) Store {
 return Store{
  db:    db,
  cache: cache,
  ttl:   ttl,
 }
}

func (s *Store) Get(key string) int {
 fromCache, err := s.cache.Get(key)
 if err != nil {
  fmt.Println("Cache miss")
  fromDb, ok := s.db[key]
  if !ok {
   return -1
  }
  s.cache.SetWithExpire(key, fromDb, s.ttl)
  return fromDb
 }
 fmt.Println("Cache hit")
 return fromCache.(int)
}

Сначала мы определяем структуру Store. Он содержит поле db, которое для простоты является картой. В производственной среде это будет клиент для базы данных, такой как Postgresql, MongoDB или любой другой, который мы выберем. Он также содержит реализацию gcache Cache, которую мы будем использовать в качестве уровня кэширования. Нам также нужно установить поле time to live, что важно, потому что обычно мы не хотим хранить запись в кеше бесконечно. Кэширование имеет смысл, когда данные меняются не часто. Если данные меняются быстро, вам нужно установить очень низкое время жизни или даже пересмотреть, является ли кэширование хорошей идеей. В случае результатов SAT они, вероятно, вообще не изменятся, однако они могут измениться, если студент решит пересдать экзамен, поэтому мы действительно хотим, чтобы результаты, хранящиеся в кеше, в какой-то момент истекли.

Мы определяем фабричный метод NewStore, который мы будем использовать в нашей основной функции для создания экземпляра этого хранилища.

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

Поскольку метод Get интерфейса gcache.Cache возвращает пустой интерфейс, нам нужно привести тип к целому числу в операторе return, чтобы выполнить ожидаемый возвращаемый тип.

Чтобы доказать, что наш механизм кэширования действительно работает, мы добавим пару операторов печати, чтобы определить, является ли это попаданием в кэш или нет.

Пакет обработчика

Теперь мы можем создать файл score.go в нашем пакете обработчика, который будет содержать код, который выполняется, когда клиент отправляет запрос в конечную точку API, которая будет получать оценку SAT:

package handler

import (
 "encoding/json"
 "net/http"

 "github.com/gorilla/mux"
 "github.com/pavledjuric/caching/pkg/store"
)

type ScoreHandler struct {
 store store.Store
}

func (s *ScoreHandler) HandleGet(w http.ResponseWriter, r *http.Request) {

 params := mux.Vars(r)
 student := params["student"]
 score := s.store.Get(student)
 if score == -1 {
  w.WriteHeader(http.StatusNotFound)
 } else {
  response := map[string]interface{}{"student": student, "score": score}
  w.WriteHeader(http.StatusOK)
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(response)

 }
}

func NewScoreHandler(s store.Store) *ScoreHandler {
 return &ScoreHandler{s}
}

Сначала мы определяем структуру ScoreHandler. Он содержит только структуру хранилища, которую мы будем использовать в качестве клиента для доступа к данным в хранилище.

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

Когда клиент звонит

ПОЛУЧИТЬ /scores/{студент

будет выполнен метод HandleGet. Он проанализирует параметр пути ученика, вызовет метод Get структуры хранилища и передаст этот параметр, и если данные не будут найдены в хранилище, мы вернем 404. Если они будут найдены, мы создадим ответ JSON путем кодирования это со встроенным пакетом JSON и вернуть код состояния 200. Довольно прямолинейно, правда?

Основной пакет

Теперь у нас, наконец, есть все, что нам нужно, поэтому мы можем написать файл main.go.

package main

import (
 "time"

 "log"
 "net/http"

 "github.com/bluele/gcache"
 "github.com/gorilla/mux"
 "github.com/pavledjuric/caching/pkg/handler"
 "github.com/pavledjuric/caching/pkg/store"
)

func main() {

 db := map[string]int{
  "Tom":   1410,
  "Jill":  1420,
  "Mike":  1290,
  "Alice": 1450,
 }

 store := store.NewStore(db, gcache.New(30).LFU().Build(), time.Hour)
 sh := handler.NewScoreHandler(store)
 r := mux.NewRouter()
 r.HandleFunc("/scores/{student}", sh.HandleGet).Methods("GET")

 srv := &http.Server{
  Handler:      r,
  Addr:         "127.0.0.1:8000",
  WriteTimeout: 15 * time.Second,
  ReadTimeout:  15 * time.Second,
 }
 log.Fatal(srv.ListenAndServe())

}

Во-первых, мы импортируем все наши внутренние пакеты и внешние зависимости.

Далее, поскольку это всего лишь пример приложения и поскольку мы не хотим тратить время на создание таблиц в СУБД, мы смоделируем нашу базу данных, создав экземпляр карты, содержащей данные.

Затем мы хотим создать экземпляр хранилища, обработчика, а также мультиплексора. Нам также необходимо сопоставить конечные точки с функциями, что в данном случае просто, поскольку весь наш сервис имеет только одну конечную точку.

Наконец, мы можем создать экземпляр HTTP-сервера и запустить его.

Исход

Давайте посмотрим, действительно ли это сработало.

Мы запустим приложение, выполнив:

go run main.go 

Затем мы можем перейти к браузеру или любому другому HTTP-клиенту и посмотреть, все ли работает:

Похоже, конечная точка работает, но давайте подтвердим, что этот первоначальный вызов был из первичной базы данных, а не из кеша:

Хорошо выглядеть. Если все работает как задумано, результаты для Тома должны храниться в кеше в течение часа. Давайте снова вызовем конечную точку и подтвердим, что на этот раз результат пришел из кеша:

Вроде все заработало! Теперь у нас есть полностью работающий сервис REST API, который кэширует результаты из основной базы данных в течение часа.

Мы узнали, что кэширование может быть очень полезным, если данные относительно статичны. Если мы хотим сократить расходы и сократить обслуживание инфраструктуры, мы можем иногда использовать кэш в памяти, если не ожидаем слишком большой нагрузки на наш сервис. Однако мы должны помнить, что если мы ожидаем хранить большой объем данных в вашем кеше, нам, возможно, следует рассмотреть решение для кэширования, такое как Redis или Memcached.

На этом все, если вам понравилось читать, вы можете поддержать меня, подписавшись на Medium по этой ссылке. Вы получите доступ ко всем статьям, написанным на Medium.

Спасибо за прочтение!