← Назад к вопросам

Приведи пример стратегии кэширования

2.0 Middle🔥 231 комментариев
#Кэширование#Микросервисы и архитектура#Производительность и оптимизация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Пример стратегии кэширования для веб-приложения на Go

В современном веб-разработке эффективное кэширование — ключевой фактор производительности. Я продемонстрирую многоуровневую стратегию на примере REST API для блога с использованием Go. Стратегия включает кэш приложения (in-memory), распределенный кэш (Redis) и HTTP-кэширование (ETag).

1. Многоуровневый подход (Layered Caching)

Основная идея — организовать кэширование от быстрых, но ограниченных слоев к более медленным, но ёмким и распределённым.

package cache

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "sync"
    "time"
    
    "github.com/go-redis/redis/v8"
)

type LayeredCache struct {
    local   *sync.Map // In-memory кэш (1-й уровень)
    remote  *redis.Client // Redis кэш (2-й уровень)
    ttlLocal, ttlRemote time.Duration
}

2. Реализация стратегии "Cache-Aside" (Lazy Loading)

Это наиболее распространённая стратегия: данные загружаются в кэш только при запросе.

func (c *LayeredCache) GetPost(ctx context.Context, id string) (*Post, error) {
    // 1. Проверяем локальный кэш
    if val, ok := c.local.Load(id); ok {
        return val.(*Post), nil
    }
    
    // 2. Проверяем Redis
    var post Post
    redisKey := fmt.Sprintf("post:%s", id)
    if err := c.remote.Get(ctx, redisKey).Scan(&post); err == nil {
        // Сохраняем в локальный кэш для будущих запросов
        c.local.Store(id, &post)
        return &post, nil
    }
    
    // 3. Загружаем из БД (предполагаем наличие метода в репозитории)
    post, err := c.loadFromDB(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // 4. Заполняем оба уровня кэша асинхронно
    go func() {
        c.local.Store(id, &post)
        c.remote.Set(ctx, redisKey, &post, c.ttlRemote)
    }()
    
    return &post, nil
}

3. Инвалидация кэша при обновлении данных

Критически важный аспект — своевременная инвалидация устаревших данных.

func (c *LayeredCache) UpdatePost(ctx context.Context, id string, updated *Post) error {
    // 1. Обновляем в БД
    if err := c.updateInDB(ctx, id, updated); err != nil {
        return err
    }
    
    // 2. Инвалидируем кэши
    c.local.Delete(id)
    redisKey := fmt.Sprintf("post:%s", id)
    if err := c.remote.Del(ctx, redisKey).Err(); err != nil {
        // Логируем ошибку, но не прерываем выполнение
        log.Printf("Redis delete failed: %v", err)
    }
    
    // 3. Для контента с высоким трафиком можно сразу записать новое значение
    if c.isHotKey(id) {
        c.local.Store(id, updated)
        c.remote.Set(ctx, redisKey, updated, c.ttlRemote)
    }
    
    return nil
}

4. HTTP-кэширование с ETag

Добавляем слой кэширования на уровне HTTP для снижения нагрузки на сервер.

func (h *PostHandler) GetPostHTTP(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    
    // Генерируем ETag на основе содержимого
    post, err := h.cache.GetPost(r.Context(), id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    // Создаём хэш от данных поста для ETag
    dataHash := sha256.Sum256([]byte(fmt.Sprintf("%v", post)))
    eTag := hex.EncodeToString(dataHash[:])
    
    // Проверяем If-None-Match заголовок от клиента
    if match := r.Header.Get("If-None-Match"); match == eTag {
        w.WriteHeader(http.StatusNotModified)
        return
    }
    
    w.Header().Set("ETag", eTag)
    w.Header().Set("Cache-Control", "public, max-age=60")
    json.NewEncoder(w).Encode(post)
}

5. Дополнительные стратегии и оптимизации

  • Write-Through: Для критически важных данных, где нужно гарантировать актуальность
func (c *LayeredCache) WriteThrough(ctx context.Context, key string, value interface{}) error {
    // 1. Сначала в БД
    if err := c.saveToDB(ctx, key, value); err != nil {
        return err
    }
    // 2. Затем в кэши
    c.local.Store(key, value)
    return c.remote.Set(ctx, key, value, c.ttlRemote).Err()
}
  • TTL с рандомизацией: Предотвращаем "Cache Stampede"
func randomizedTTL(baseTTL time.Duration, variance time.Duration) time.Duration {
    rand.Seed(time.Now().UnixNano())
    offset := time.Duration(rand.Int63n(int64(variance)))
    return baseTTL + offset
}

6. Мониторинг и метрики

Эффективная стратегия требует мониторинга:

  • Hit/Miss Ratio по уровням кэширования
  • Latency для каждого слоя
  • Memory usage локального кэша
type CacheMetrics struct {
    LocalHit   prometheus.Counter
    LocalMiss  prometheus.Counter
    RemoteHit  prometheus.Counter
    RemoteMiss prometheus.Counter
    DBLoad     prometheus.Counter
}

Ключевые принципы успешной стратегии:

  1. Многоуровневость: Локальный кэш для горячих данных, Redis для распределенного доступа
  2. Согласованность: Чёткая политика инвалидации при записи
  3. Отказоустойчивость: Кэш не должен быть single point of failure
  4. Адаптивность: TTL и размеры кэшей должны настраиваться под паттерны доступа
  5. Экономичность: Избегаем кэширования редко запрашиваемых данных

Эта стратегия позволяет обрабатывать тысячи запросов в секунду с минимальной нагрузкой на базу данных, обеспечивая при этом актуальность данных. Важно протестировать её под конкретную нагрузку и адаптировать параметры (размеры кэшей, TTL) под реальные паттерны доступа в вашем приложении.