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

Синхронизация кэша

2.3 Middle🔥 241 комментариев
#Конкурентность и горутины#Кэширование

Условие

Реализуйте потокобезопасный кэш с TTL (время жизни записей).

Интерфейс

type Cache struct {
    // ваши поля
}

func NewCache(cleanupInterval time.Duration) *Cache
func (c *Cache) Set(key string, value interface{}, ttl time.Duration)
func (c *Cache) Get(key string) (interface{}, bool)
func (c *Cache) Delete(key string)

Требования

  • Потокобезопасность (concurrent access)
  • Автоматическое удаление устаревших записей
  • Фоновая очистка с заданным интервалом
  • Graceful shutdown для фоновой горутины

Пример

cache := NewCache(time.Minute)
cache.Set("key1", "value1", 5*time.Minute)
val, ok := cache.Get("key1") // "value1", true

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

Синхронизация кэша с TTL - полное решение

Описание задачи

Нужно реализовать потокобезопасный кэш, который:

  • Хранит значения с ограничением по времени (TTL)
  • Автоматически удаляет устаревшие записи
  • Поддерживает concurrent access через RWMutex
  • Корректно завершает фоновую горутину очистки

Архитектурное решение

Компоненты:

  1. RWMutex - для потокобезопасности (читать часто, писать редко)
  2. Структура Entry - хранит значение и время жизни
  3. *map[string]Entry - основное хранилище
  4. Фоновая горутина - периодически удаляет устаревшие записи
  5. Канал для shutdown - graceful завершение

Реализация

package main

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

// Entry хранит значение и время его истечения
type Entry struct {
    Value     interface{}
    ExpiresAt time.Time
}

// Cache потокобезопасный кэш с TTL
type Cache struct {
    mu                sync.RWMutex
    data              map[string]*Entry
    cleanupInterval   time.Duration
    stopCh            chan struct{}
    wg                sync.WaitGroup
}

// NewCache создает новый кэш с фоновой очисткой
func NewCache(cleanupInterval time.Duration) *Cache {
    c := &Cache{
        data:            make(map[string]*Entry),
        cleanupInterval: cleanupInterval,
        stopCh:          make(chan struct{}),
    }
    
    // Запускаем фоновую горутину очистки
    c.wg.Add(1)
    go c.cleanupWorker()
    
    return c
}

// Set добавляет или обновляет значение в кэше
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[key] = &Entry{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

// Get получает значение из кэша
func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    entry, exists := c.data[key]
    if !exists {
        return nil, false
    }
    
    // Проверяем не истекло ли время жизни
    if time.Now().After(entry.ExpiresAt) {
        return nil, false
    }
    
    return entry.Value, true
}

// Delete удаляет значение из кэша
func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    delete(c.data, key)
}

// cleanupWorker фоновая горутина для удаления устаревших записей
func (c *Cache) cleanupWorker() {
    defer c.wg.Done()
    
    ticker := time.NewTicker(c.cleanupInterval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // Удаляем устаревшие записи
            c.cleanup()
        
        case <-c.stopCh:
            // Сигнал завершения
            return
        }
    }
}

// cleanup удаляет все устаревшие записи
func (c *Cache) cleanup() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    now := time.Now()
    for key, entry := range c.data {
        if now.After(entry.ExpiresAt) {
            delete(c.data, key)
        }
    }
}

// Stop gracefully завершает работу кэша
func (c *Cache) Stop() {
    close(c.stopCh)
    c.wg.Wait()
}

func main() {
    fmt.Println("=== Cache with TTL ===")
    
    // Создаем кэш с интервалом очистки 100ms
    cache := NewCache(100 * time.Millisecond)
    defer cache.Stop()
    
    // Добавляем значения
    cache.Set("key1", "value1", 500*time.Millisecond)
    cache.Set("key2", "value2", 1*time.Second)
    cache.Set("key3", "value3", 50*time.Millisecond)
    
    // Проверяем значения
    fmt.Println("Immediately after Set:")
    if val, ok := cache.Get("key1"); ok {
        fmt.Printf("key1 = %v\\n", val)
    }
    
    // Ждем когда key3 истечет
    time.Sleep(100 * time.Millisecond)
    fmt.Println("\\nAfter 100ms (key3 should expire):")
    if val, ok := cache.Get("key3"); ok {
        fmt.Printf("key3 = %v\\n", val)
    } else {
        fmt.Println("key3 has expired")
    }
    
    // key1 и key2 еще живы
    if val, ok := cache.Get("key1"); ok {
        fmt.Printf("key1 = %v\\n", val)
    }
    
    // Ждем когда key1 истечет
    time.Sleep(500 * time.Millisecond)
    fmt.Println("\\nAfter 500ms more (key1 should expire):")
    if val, ok := cache.Get("key1"); ok {
        fmt.Printf("key1 = %v\\n", val)
    } else {
        fmt.Println("key1 has expired")
    }
}

Улучшенная версия с методом для получения статистики

// Stats возвращает статистику кэша
func (c *Cache) Stats() map[string]interface{} {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    count := 0
    totalSize := 0
    
    now := time.Now()
    for _, entry := range c.data {
        if now.Before(entry.ExpiresAt) {
            count++
            // Примерный размер в байтах
            totalSize += 100
        }
    }
    
    return map[string]interface{}{
        "active_entries": count,
        "estimated_size_bytes": totalSize,
    }
}

// Contains проверяет наличие ключа (без получения значения)
func (c *Cache) Contains(key string) bool {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    entry, exists := c.data[key]
    if !exists {
        return false
    }
    
    return time.Now().Before(entry.ExpiresAt)
}

Версия с максимальным размером кэша

type CacheWithLimit struct {
    mu              sync.RWMutex
    data            map[string]*Entry
    maxSize         int
    cleanupInterval time.Duration
    stopCh          chan struct{}
    wg              sync.WaitGroup
}

func NewCacheWithLimit(maxSize int, cleanupInterval time.Duration) *CacheWithLimit {
    c := &CacheWithLimit{
        data:            make(map[string]*Entry),
        maxSize:         maxSize,
        cleanupInterval: cleanupInterval,
        stopCh:          make(chan struct{}),
    }
    c.wg.Add(1)
    go c.cleanupWorker()
    return c
}

func (c *CacheWithLimit) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // Если достигли лимита, удаляем самую старую запись
    if len(c.data) >= c.maxSize {
        oldestKey := ""
        oldestTime := time.Now()
        for k, entry := range c.data {
            if entry.ExpiresAt.Before(oldestTime) {
                oldestKey = k
                oldestTime = entry.ExpiresAt
            }
        }
        if oldestKey != "" {
            delete(c.data, oldestKey)
        }
    }
    
    c.data[key] = &Entry{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

Анализ сложности

Get:

  • Time: O(1) - проверка в map
  • Space: O(1)

Set:

  • Time: O(1) - добавление в map
  • Space: O(1)

Delete:

  • Time: O(1) - удаление из map
  • Space: O(1)

Cleanup:

  • Time: O(n) где n - количество записей в кэше
  • Space: O(1)

Best Practices

  1. Используй RWMutex - для кэша читают часто, пишут редко
  2. Проверяй TTL при Get - быстро исключает устаревшие данные
  3. Graceful shutdown - всегда закрывай stopCh и жди WaitGroup
  4. Таймер в cleanup - используй time.NewTicker для периодических операций
  5. Документируй поведение - четко описывай когда удаляются записи

Краевые случаи

  • Получение ключа после его истечения - возвращает false
  • Обновление значения продляет TTL - работает как новое значение
  • Множественные горутины читают одновременно - RWMutex обеспечивает параллелизм
  • Вызов Get во время cleanup - не вызывает deadlock благодаря RWMutex