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

Какие знаешь стратегии инвалидации кэша?

2.3 Middle🔥 181 комментариев
#Кэширование и NoSQL

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

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

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

Стратегии инвалидации кэша

Инвалидация кэша — одна из двух сложнейших задач в компьютерных науках (вместе с именованием переменных). Вот главные стратегии.

1. Time-Based Expiration (TTL)

Самая простая: кэш автоматически истекает через определённое время:

@Cacheable(value = "users", unless = "#result == null")
@Transactional(readOnly = true)
public User getUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

// В конфигурации
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)  // TTL = 10 минут
            .maximumSize(1000));
        return cacheManager;
    }
}

Плюсы: просто, не требует логики инвалидации Минусы: кэш может быть устаревшим, или просроченным раньше нужного

2. Event-Based Invalidation (явная инвалидация)

Инвалидируй кэш при изменении данных:

@Service
public class UserService {
    
    @Autowired
    private CacheManager cacheManager;
    
    @CacheEvict(value = "users", key = "#id")  // удали из кэша при обновлении
    @Transactional
    public User updateUser(Long id, User user) {
        User existing = userRepository.findById(id).orElseThrow();
        existing.setName(user.getName());
        existing.setEmail(user.getEmail());
        return userRepository.save(existing);
    }
    
    @CacheEvict(value = "users", allEntries = true)  // очисть весь кэш
    @Transactional
    public void deleteAllUsers() {
        userRepository.deleteAll();
    }
    
    @Transactional
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
        cacheManager.getCache("users").evict(id);  // явная инвалидация
    }
}

Плюсы: кэш всегда актуален Минусы: нужно помнить инвалидировать везде

3. Write-Through (запись сквозь кэш)

При записи обновляем и кэш, и БД:

@Service
public class UserCacheService {
    
    @Autowired
    private UserRepository repository;
    
    @Autowired
    private CacheManager cacheManager;
    
    public User saveUser(User user) {
        // 1. Сохраняем в кэш
        Cache cache = cacheManager.getCache("users");
        cache.put(user.getId(), user);
        
        // 2. Сохраняем в БД
        User saved = repository.save(user);
        
        // 3. Обновляем кэш с сохранённой версией
        cache.put(saved.getId(), saved);
        
        return saved;
    }
}

Плюсы: кэш и БД всегда синхронизированы Минусы: медленнее, дорого при сбое БД

4. Write-Behind (запись позади кэша)

Сначала в кэш, потом асинхронно в БД:

@Service
public class AsyncUserService {
    
    @Autowired
    private CacheManager cacheManager;
    
    @Autowired
    private UserRepository repository;
    
    public User updateUser(Long id, User user) {
        // 1. Немедленно обновляем кэш
        Cache cache = cacheManager.getCache("users");
        user.setId(id);
        cache.put(id, user);
        
        // 2. Асинхронно обновляем БД
        saveToDatabase(user);
        
        return user;
    }
    
    @Async
    private void saveToDatabase(User user) {
        try {
            repository.save(user);
        } catch (Exception e) {
            // Логируем ошибку, но клиент уже получил ответ
            log.error("Failed to save user", e);
        }
    }
}

Плюсы: быстро для клиента Минусы: риск потери данных, сложно

5. LRU/LFU Eviction (вытеснение по использованию)

Удаляй редко используемые элементы:

CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
    .maximumSize(1000)  // максимум 1000 элементов
    .recordStats()      // отслеживать статистику
    .build());

// LRU (Least Recently Used) — удаляет давно не использовавшиеся
// LFU (Least Frequently Used) — удаляет редко использовавшиеся

6. Lazy Invalidation (ленивая инвалидация)

Проверяй актуальность при получении:

@Service
public class LazyUserService {
    
    @Autowired
    private CacheManager cacheManager;
    
    public User getUser(Long id) {
        Cache cache = cacheManager.getCache("users");
        User cached = cache.get(id, User.class);
        
        if (cached != null) {
            // Проверяем версию в БД
            User fresh = repository.findById(id).orElse(null);
            if (fresh != null && !fresh.getVersion().equals(cached.getVersion())) {
                cache.evict(id);  // версия изменилась, инвалидируем
                return fresh;
            }
            return cached;
        }
        
        return repository.findById(id).orElse(null);
    }
}

7. Tag-Based Invalidation (инвалидация по тегам)

Группируй кэши и инвалидируй группы:

@Service
public class UserService {
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }
    
    @CacheEvict(value = "users", allEntries = true)  // очисть весь кэш
    public void bulkUpdate(List<User> users) {
        userRepository.saveAll(users);
    }
}

8. Redis Pattern Invalidation

В Redis инвалидируй по паттернам:

@Service
public class RedisCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void invalidateUserCache(Long userId) {
        // Удаляй все ключи, содержащие userId
        Set<String> keys = redisTemplate.keys("user:" + userId + ":*");
        redisTemplate.delete(keys);
    }
    
    public void invalidateAllUserCache() {
        Set<String> keys = redisTemplate.keys("user:*");
        redisTemplate.delete(keys);
    }
}

9. Версионирование сущностей

Трекируй версию и инвалидируй при изменении:

@Entity
public class User {
    @Id private Long id;
    private String name;
    
    @Version
    private Long version;  // JPA optimistic locking
}

// При сохранении версия меняется автоматически
user.setName("New Name");
repository.save(user);  // version будет +1

Таблица сравнения стратегий

СтратегияАктуальностьПроизводительностьСложностьИспользуй когда
TTLСредняяВысокаяНизкаяНекритичные данные
Event-BasedВысокаяСредняяСредняяКритичные данные
Write-ThroughОчень высокаяНизкаяВысокаяФинансовые данные
Write-BehindСредняяВысокаяВысокаяВысоконагруженные системы
LRU/LFUНизкаяСредняяНизкаяОграниченная память

Реальный пример: комбинированная стратегия

@Service
public class HybridUserService {
    
    // TTL + Event-Based
    @Cacheable(value = "users", key = "#id")
    @Transactional(readOnly = true)
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @CacheEvict(value = "users", key = "#id")
    @Transactional
    public User updateUser(Long id, User user) {
        User existing = userRepository.findById(id).orElseThrow();
        existing.update(user);
        return userRepository.save(existing);
    }
}

// Конфигурация
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cm = new CaffeineCacheManager("users");
        cm.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(30, TimeUnit.MINUTES)  // TTL
            .maximumSize(10000))                     // LRU
        return cm;
    }
}

Итого

  • TTL — простая, для некритичных данных
  • Event-Based — надёжная, для критичных данных
  • Write-Through — для финансовых данных
  • Write-Behind — для высоконагруженных систем
  • Комбинированная — лучший выбор в большинстве случаев (TTL + Event-Based)