Какие знаешь стратегии инвалидации кэша?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегии инвалидации кэша
Инвалидация кэша — одна из двух сложнейших задач в компьютерных науках (вместе с именованием переменных). Вот главные стратегии.
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)