Приведи пример стратегии кэширования
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример стратегии кэширования для веб-приложения на 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
}
Ключевые принципы успешной стратегии:
- Многоуровневость: Локальный кэш для горячих данных, Redis для распределенного доступа
- Согласованность: Чёткая политика инвалидации при записи
- Отказоустойчивость: Кэш не должен быть single point of failure
- Адаптивность: TTL и размеры кэшей должны настраиваться под паттерны доступа
- Экономичность: Избегаем кэширования редко запрашиваемых данных
Эта стратегия позволяет обрабатывать тысячи запросов в секунду с минимальной нагрузкой на базу данных, обеспечивая при этом актуальность данных. Важно протестировать её под конкретную нагрузку и адаптировать параметры (размеры кэшей, TTL) под реальные паттерны доступа в вашем приложении.