
Redis — хранилище из семейства нереляционных (NoSQL) баз данных. Redis является очень быстрым хранилищем данных благодаря своей архитектуре in-memory. Он идеально подходит для задач, требующих быстрого доступа к данным, таких как кэширование, очереди сообщений, сессионная информация и многое другое. Go также известен своей высокой производительностью за счет компиляции в машинный код и эффективного управления памятью.
Установка
В качестве клиента для Redis будем использовать библиотеку go-redis
go get github.com/redis/go-redis/v9Для начала создадим новое соединение с базой данных. Первым делом создадим небольшую структуру которая будет хранить в себе информацию о конфигурации:
// storage/redis.go
type Config struct {
	Addr        string        `yaml:"addr"`
	Password    string        `yaml:"password"`
	User        string        `yaml:"user"`
	DB          int           `yaml:"db"`
	MaxRetries  int           `yaml:"max_retries"`
	DialTimeout time.Duration `yaml:"dial_timeout"`
	Timeout     time.Duration `yaml:"timeout"`
}Где Addr - адрес нашей базы данных, Password - пароль, User - имя пользователя, DB - идентификатор базы данных, MaxRetries - максимальное количество попыток подключения, DialTimeout - таймаут для установления новых соединений, Timeout - таймаут для записи и чтения.
Теперь пропишем функцию для создания нового соединения:
// storage/redis.go
func NewClient(ctx context.Context, cfg Config) (*redis.Client, error) {
	db := redis.NewClient(&redis.Options{
		Addr:         cfg.Addr,
		Password:     cfg.Password,
		DB:           cfg.DB,
		Username:     cfg.User,
		MaxRetries:   cfg.MaxRetries,
		DialTimeout:  cfg.DialTimeout,
		ReadTimeout:  cfg.Timeout,
		WriteTimeout: cfg.Timeout,
	})
	if err := db.Ping(ctx).Err(); err != nil {
		fmt.Printf("failed to connect to redis server: %s\n", err.Error())
		return nil, err
	}
	return db, nil
}
Примеры записи и получения данных
// main.go
package main
func main() {
  cfg := storage.Config{
      Addr:        "localhost:6379",
      Password:    "test1234",
      User:        "testuser",
      DB:          0,
      MaxRetries:  5,
      DialTimeout: 10 * time.Second,
      Timeout:     5 * time.Second,
  }
  db, err := storage.NewClient(context.Background(), cfg)
  if err != nil {
      panic(err)
  }
  // Запись данных
  // db.Set(контекст, ключ, значение, время жизни в базе данных)
  if err := db.Set(context.Background(), "key", "test value", 0).Err(); err != nil {
      fmt.Printf("failed to set data, error: %s", err.Error())
  }
  if err := db.Set(context.Background(), "key2", 333, 30*time.Second).Err(); err != nil {
      fmt.Printf("failed to set data, error: %s", err.Error())
  }
  // Получение данных
  
  val, err := db.Get(context.Background(), "key").Result()
  if err == redis.Nil {
      fmt.Println("value not found")
  } else if err != nil {
      fmt.Printf("failed to get value, error: %v\n", err)
  }
  val2, err := db.Get(context.Background(), "key2").Result()
  if err == redis.Nil {
      fmt.Println("value not found")
  } else if err != nil {
      fmt.Printf("failed to get value, error: %v\n", err)
  }
  fmt.Printf("value: %v\n", val)
  fmt.Printf("value: %v\n", val2)
}Пример кэширования данных
Как было сказано ранее Redis является очень быстрым хранилищем данных и используется для хранения кэша. В качестве примера реализуем следующий кейс:
Существует API сервер у которого существует единственная ручка - получение карточек с информацией, карточки хранятся в базе данных и их получение является дорогой по времени операцией. Для решения данной задачи предлагается сохранять полученную карточку в кэш и хранить ее там 30 секунд, при повторном запросе карточки она будет возвращаться из кэша.
Выше мы уже реализовали пример соединения с базой данных Redis поэтому перенесем его в наш проект
// main.go
package main
func main() {
  cfg := storage.Config{
      Addr:        "localhost:6379",
      Password:    "test1234",
      User:        "testuser",
      DB:          0,
      MaxRetries:  5,
      DialTimeout: 10 * time.Second,
      Timeout:     5 * time.Second,
  }
  db, err := storage.NewClient(context.Background(), cfg)
  if err != nil {
      panic(err)
  }
  
}Теперь создадим API ручку которая будет возвращать пользователю карточку. Для начала установим библиотеку chi и chi render :
go get github.com/go-chi/chi/v5go get github.com/go-chi/renderСоздадим структуру нашей карточки
// handlers/cache.go
type Card struct {
	ID   int    `json:"id" redis:"id"`
	Name string `json:"name" redis:"name"`
	Data string `json:"data" redis:"data"`
}Для получения карточек создадим API ручку
// handlers/cache.go
func GetCard(w http.ResponseWriter, r *http.Request) {
  
  // Имитируем долгое обрашение в базу данных для получения карточки
  time.Sleep(3 * time.Second)
  // Получаем ID карточки из URL запроса
  idStr := chi.URLParam(r, "id")
  if idStr == "" {
      render.Status(r, http.StatusBadRequest)
      return
  }
  // Преобразуем ID из строки в целое число
  id, err := strconv.Atoi(idStr)
  if err != nil {
      render.Status(r, http.StatusBadRequest)
      return
  }
  card := Card{
      ID:   id,
      Name: "Test Card",
      Data: "This is a test card.",
  }
  
  render.Status(r, 200)
  render.JSON(w, r, card)
}Настало время научиться сохранять структуры в хранилище Redis, если прибегнуть к официальной документации то мы увидим следующую реализацию:
type Model struct {
	Str1    string   `redis:"str1"`
	Str2    string   `redis:"str2"`
	Int     int      `redis:"int"`
	Bool    bool     `redis:"bool"`
	Ignored struct{} `redis:"-"`
}
rdb := redis.NewClient(&redis.Options{
	Addr: ":6379",
})
if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error {
	rdb.HSet(ctx, "key", "str1", "hello")
	rdb.HSet(ctx, "key", "str2", "world")
	rdb.HSet(ctx, "key", "int", 123)
	rdb.HSet(ctx, "key", "bool", 1)
	return nil
}); err != nil {
	panic(err)
}Сразу можно обратить внимание что каждое поле структуры необходимо сохранять в отдельной строке вручную. Можно воспользоваться данным примером, но мы пойдем немного дальше и напишем свою реализацию в которой не будет необходимости прописывать каждое поле вручную, мы реализуем решение данной проблемы в качестве метода структуры, но вы можете вынести его в отдельную функцию, чтобы использовать ее для других структур.
// handlers/cache.go
func (c *Card) ToRedisSet(ctx context.Context, db *redis.Client, key string) error {
  // Получаем элементы структуры
  val := reflect.ValueOf(c).Elem()
  // Создаем функцию для записи структуры в хранилище
  settter := func(p redis.Pipeliner) error {
    // Итерируемся по полям структуры
    for i := 0; i < val.NumField(); i++ {
        field := val.Type().Field(i)
        // Получаем содержимое тэга redis
        tag := field.Tag.Get("redis")
        // Записываем значение поля и содержимое тэга redis в хранилище
        if err := p.HSet(ctx, key, tag, val.Field(i).Interface()).Err(); err != nil {
            return err
        }
    }
    // Задаем время хранения 30 секунд
    if err := p.Expire(ctx, key, 30*time.Second).Err(); err != nil {
        return err
    }
    return nil
  }
  // Сохраняем структуру в хранилище
  if _, err := db.Pipelined(ctx, settter); err != nil {
      return err
  }
  return nil
}Важное примечание: данная реализация не подходит если в структуре есть массивы или вложенные структуры
Следующим шагом добавим сохранение карточки в нашу API ручку, после нескольких дополнений она будет выглядеть так:
// handlers/cache.go
func GetCard(ctx context.Context, db *redis.Client) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    time.Sleep(3 * time.Second)
    idStr := chi.URLParam(r, "id")
    if idStr == "" {
        render.Status(r, http.StatusBadRequest)
        return
    }
    id, err := strconv.Atoi(idStr)
    if err != nil {
        render.Status(r, http.StatusBadRequest)
        return
    }
    card := Card{
        ID:   id,
        Name: "Test Card",
        Data: "This is a test card.",
    }
    // Сохраняем карточку в хранилище Redis на 30 секунд
    if err := card.ToRedisSet(ctx, db, idStr); err != nil {
        render.Status(r, http.StatusInternalServerError)
        return
    }
    render.Status(r, 200)
    render.JSON(w, r, card)
  }
}Когда у нас готовая ручка можно приступить к созданию middleware который будет проверять существует ли запрашиваемая карточка в хранилище Redis и в случае обнаружения, возвращать ее клиенту:
// handlers/cache.go
func CacheMiddleware(ctx context.Context, db *redis.Client) func(http.Handler) http.Handler {
  return func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      // Получаем ID карточки из URL запроса
      idStr := chi.URLParam(r, "id")
      if idStr == "" {
        render.Status(r, http.StatusBadRequest)
        return
      }
      // Делаем запрос в хранилище Redis
      data := new(Card)
      if err := db.HGetAll(ctx, idStr).Scan(data); err == nil && (*data != Card{}) {
        // Если удалось найти карточку, то возвращаем ее
        render.JSON(w, r, data)
        return
      }
      // Если карточку не удалось найти, то перенаправляем запрос на нашу API ручку
      next.ServeHTTP(w, r)
    })
  }
}Осталось совместить нашу ручку и middleware
// handlers/cache.go
func NewCardHandler(ctx context.Context, db *redis.Client) func(r chi.Router) {
  return func(r chi.Router) {
    r.With(CacheMiddleware(ctx, db)).
        Get("/{id}", GetCard(ctx, db))
  }
}Вот мы и на финишной прямой, теперь необходимо добавить handler в main.go
// main.go
package main
import (
	"context"
	"net/http"
	"redis/handlers"
	"redis/storage"
	"time"
	"github.com/go-chi/chi/v5"
)
func main() {
	cfg := storage.Config{
		Addr:        "localhost:6379",
		Password:    "test1234",
		User:        "testuser",
		DB:          0,
		MaxRetries:  5,
		DialTimeout: 10 * time.Second,
		Timeout:     5 * time.Second,
	}
	db, err := storage.NewClient(context.Background(), cfg)
	if err != nil {
		panic(err)
	}
	router := chi.NewRouter()
	router.Route("/card", handlers.NewCardHandler(context.Background(), db))
	srv := http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	if err := srv.ListenAndServe(); err != nil {
		panic(err)
	}
}Протестируем реализацию

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

А на втором запросе время ожидания составило 4 миллисекунды, значит карточка была получена из кэша.
В результате мы смогли реализовать простейшую систему кэширования данный для API сервиса.
Комментарии (5)
 - nskforward20.11.2024 15:34- Лично мне в вашем коде не хватает интерфейсов и отвязка в обработчике от деталей реализации конкретного репозитория. 
 - nee7720.11.2024 15:34- API ручка - добавить handler в main.go - Правильно вот так: добавить ручку в главный.иду 
 
           
 

evgeniy_kudinov
Вам не кажется, что у вас именование функции
GetCard не совпадает с тем что там внутри происходит в ToRedisSet?Вероятно, опечатка или что-то удалили из кода, что соответствовало логике функции.