8 вопросов
Снижение задержки и нагрузки: часто запрашиваемые или тяжелые данные хранят в быстром хранилище (память, Redis). Меньше обращений к БД и внешним API. В Go кеш в памяти - map + sync.RWMutex или sync.Map; для распределенного - Redis (go-redis). TTL и инвалидация задают под бизнес-логику.
var cache = struct {
sync.RWMutex
m map[string]cached
}{m: make(map[string]cached)}LRU (Least Recently Used) - вытеснение наименее недавно использованных при переполнении. В Go: hashmap + двусвязный список по времени доступа или готовые реализации (hashicorp/golang-lru, karlseguin/ccache). Задают максимальный размер; при добавлении сверх лимита удаляют самый старый элемент. Подходит для ограниченного in-memory кеша.
lru, _ := lru.New(1000)
lru.Add("key", value)
if v, ok := lru.Get("key"); ok { ... }TTL - данные устаревают по времени; при записи в БД можно сбрасывать ключ или обновлять значение. Invalidate on write: при обновлении сущности удалять/обновлять кеш. Версия в ключе: key = "user:123:v2" при смене схемы. Event-based: подписка на события изменения и инвалидация. В Go комбинируют TTL (защита от старых данных) и явную инвалидацию при записи.
func (s *Service) UpdateUser(ctx context.Context, u *User) error {
if err := s.repo.Update(ctx, u); err != nil { return err }
s.cache.Delete("user:" + u.ID)
return nil
}Cache-Aside: приложение читает кеш; при промахе читает БД, кладет в кеш, возвращает. Запись - в БД, затем инвалидация или обновление кеша. Write-Through: запись идет в кеш, кеш сам пишет в БД (или слой кеша синхронно обновляет БД). В Go Cache-Aside реализуют в сервисном слое; Write-Through - обертка над репозиторием с синхронной записью в кеш и БД.
v, err := c.Get(ctx, key)
if err == nil { return v, nil }
v, err = repo.Get(ctx, id)
if err != nil { return nil, err }
c.Set(ctx, key, v, ttl)
return v, nilStampede - одновременное истечение TTL у многих ключей, лавина запросов к БД. Решения: singleflight - один запрос обновляет кеш, остальные ждут результата (golang.org/x/sync/singleflight). Или блокировка (mutex по ключу): первый захватывает, остальные ждут или читают старое. Случайный разброс TTL уменьшает одновременное истечение.
var group singleflight.Group
v, err, _ := group.Do(key, func() (interface{}, error) {
return loadFromDB(ctx, key)
})Кеш (get/set с TTL), сессии, rate limiting (INCR + EXPIRE), очереди (List, Streams), pub/sub, блокировки (SET NX), счетчики. В Go клиент go-redis; операции контекстные (таймаут, отмена). Выбор структуры данных под задачу: строки для кеша, списки для очередей, sorted sets для рейтингов и отложенных задач.
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
err := rdb.Set(ctx, "key", "value", 10*time.Second).Err()
val, err := rdb.Get(ctx, "key").Result()Memcached - только ключ-значение (строки), многопоточный, проще. Redis - структуры данных, однопоточная модель, персистентность, pub/sub, Lua. Для простого кеша оба подходят; Redis гибче (TTL на полях, структуры). В Go для Redis - go-redis; для Memcached - bradfitz/gomemcache. Выбор: Memcached при максимальной простоте и фокусе на кеше; Redis при нужде в очередях, сессиях, сложных структурах.
Один ключ с очень высокой частотой запросов перегружает один узел (Redis) или создает конкуренцию. Решения: локальный кеш перед распределенным (двухуровневый кеш); репликация горячих ключей (несколько ключей с тем же значением); шардирование ключа на несколько (key:1, key:2) и выбор по хешу. В Go реализуют L1 in-memory кеш с коротким TTL поверх Redis.
if v, ok := localCache.Get(key); ok { return v }
v, err := redis.Get(ctx, key)
if err == nil { localCache.Set(key, v, 10*time.Second) }
return v, err