Кэширование в Go: Redis, Memcached и встроенные решения от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему кэширование критически важно для Go-приложений

В современной веб-разработке скорость отклика приложения напрямую влияет на пользовательский опыт и конверсию. Язык Go (Golang) славится своей производительностью, но даже самое оптимизированное приложение может столкнуться с узкими местами при работе с базами данных или внешними API. Здесь на помощь приходит кэширование.

Кэширование — это временное хранение часто запрашиваемых данных в быстром хранилище. В экосистеме Go существует три основных подхода:

  • Встроенные решения (sync.Map, map + мьютекс) — для небольших объёмов данных внутри одного процесса.
  • Memcached — распределённый кэш в памяти, простой и быстрый.
  • Redis — продвинутое хранилище с поддержкой структур данных, персистентности и кластеризации.

В этой статье мы разберём каждый вариант с практическими примерами на Go, сравним их производительность и дадим рекомендации по выбору.

Встроенное кэширование в Go: sync.Map и конкурентные карты

Для простых сценариев, когда кэш не должен быть распределённым, Go предлагает стандартные средства. Самый популярный вариант — sync.Map, представленный в Go 1.9.

Пример: sync.Map для кэширования результатов API

package main

import (
    "fmt"
    "sync"
    "time"
)

type Cache struct {
    data sync.Map
}

func (c *Cache) Get(key string) (interface{}, bool) {
    return c.data.Load(key)
}

func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.data.Store(key, value)
    time.AfterFunc(ttl, func() {
        c.data.Delete(key)
    })
}

func main() {
    cache := &Cache{}
    cache.Set("user_42", "Alice", 5*time.Second)

    if val, ok := cache.Get("user_42"); ok {
        fmt.Println("Кэш содержит:", val)
    }
}

Плюсы: нулевая зависимость, потокобезопасность, простота.

Минусы: нет автоматической эвакуации (кроме TTL через костыли), нет распределённости, ограниченная производительность при высокой конкуренции.

Для более сложных сценариев используйте библиотеки вроде go-cache или bigcache, которые предлагают TTL, LFU/LRU эвакуацию и низкую задержку.

Memcached в Go: лёгкий и быстрый распределённый кэш

Memcached — это распределённая система кэширования в памяти. Она идеально подходит для хранения простых пар "ключ-значение" небольшого размера (до 1 МБ).

Подключение к Memcached из Go

Для работы с Memcached используйте библиотеку github.com/bradfitz/gomemcache.

package main

import (
    "fmt"
    "github.com/bradfitz/gomemcache/memcache"
)

func main() {
    mc := memcache.New("localhost:11211")

    // Запись в кэш
    err := mc.Set(&memcache.Item{
        Key:   "user_100",
        Value: []byte(`{"name":"Bob","age":30}`),
        Expiration: 300, // 5 минут
    })
    if err != nil {
        panic(err)
    }

    // Чтение из кэша
    item, err := mc.Get("user_100")
    if err == nil {
        fmt.Printf("Данные: %s\n", item.Value)
    } else {
        fmt.Println("Промах кэша")
    }
}

Когда выбирать Memcached:

  • Вам нужно простое хранилище ключ-значение.
  • Размер объектов не превышает 1 МБ.
  • Не требуется персистентность или сложные структуры данных.
  • Высокая скорость записи/чтения (латентность < 1 мс).

Ограничения: нет репликации, нет транзакций, данные теряются при перезапуске.

Redis в Go: мощный инструмент с богатыми возможностями

Redis — это не просто кэш, а полноценное хранилище данных в памяти с поддержкой строк, списков, множеств, хэшей и даже очередей. Для Go лучшим клиентом считается github.com/redis/go-redis/v9.

Пример: кэширование с Redis и обработка промахов

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var ctx = context.Background()

func getUserFromDB(id int) (*User, error) {
    time.Sleep(100 * time.Millisecond) // симуляция БД
    return &User{ID: id, Name: "Alice"}, nil
}

func getCachedUser(rdb *redis.Client, id int) (*User, error) {
    key := fmt.Sprintf("user:%d", id)

    // 1. Проверяем кэш
    val, err := rdb.Get(ctx, key).Result()
    if err == nil {
        var user User
        json.Unmarshal([]byte(val), &user)
        return &user, nil
    }

    // 2. Промах — загружаем из БД
    user, err := getUserFromDB(id)
    if err != nil {
        return nil, err
    }

    // 3. Сохраняем в кэш на 1 час
    data, _ := json.Marshal(user)
    rdb.Set(ctx, key, data, 1*time.Hour)

    return user, nil
}

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    user, _ := getCachedUser(rdb, 42)
    fmt.Printf("Получен пользователь: %+v\n", user)
}

Дополнительные возможности Redis

  • TTL и автоматическая эвакуация — встроенная поддержка времени жизни ключей.
  • Структуры данных — списки, множества, хэши, геоданные.
  • Персистентность — RDB и AOF для сохранения на диск.
  • Кластеризация — горизонтальное масштабирование.
  • Pub/Sub — уведомления между сервисами.

Недостатки: потребляет больше памяти, чем Memcached; сложнее в настройке; немного медленнее на простых операциях.

Блоги

Рекомендаций по книгам