Использование Redis в Go
- четверг, 21 ноября 2024 г. в 00:00:04
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 сервиса.