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/
Добавить комментарий