Использование Redis в Go

от автора

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/v5
go 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 сервиса.


ссылка на оригинал статьи https://habr.com/ru/articles/860060/