← Назад к вопросам
Синхронизация кэша
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
- Корректно завершает фоновую горутину очистки
Архитектурное решение
Компоненты:
- RWMutex - для потокобезопасности (читать часто, писать редко)
- Структура Entry - хранит значение и время жизни
- *map[string]Entry - основное хранилище
- Фоновая горутина - периодически удаляет устаревшие записи
- Канал для 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
- Используй RWMutex - для кэша читают часто, пишут редко
- Проверяй TTL при Get - быстро исключает устаревшие данные
- Graceful shutdown - всегда закрывай stopCh и жди WaitGroup
- Таймер в cleanup - используй time.NewTicker для периодических операций
- Документируй поведение - четко описывай когда удаляются записи
Краевые случаи
- Получение ключа после его истечения - возвращает false
- Обновление значения продляет TTL - работает как новое значение
- Множественные горутины читают одновременно - RWMutex обеспечивает параллелизм
- Вызов Get во время cleanup - не вызывает deadlock благодаря RWMutex