Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Кэширование в Spring Framework
Spring предоставляет встроенный механизм кэширования, который позволяет сохранять результаты методов для избежания дорогостоящих операций.
Архитектура кэширования в Spring
┌─────────────────────────────────────┐
│ @Cacheable / @CachePut / @CacheEvict
│ (Аннотации)
│
├─────────────────────────────────────┤
│ CacheManager
│ - Управляет кэшами
│ - Выбирает реализацию
│
├─────────────────────────────────────┤
│ Cache Interface
│ - get(key) → value
│ - put(key, value)
│ - evict(key)
│
├─────────────────────────────────────┤
│ Реализации
│ - SimpleCacheManager (память)
│ - ConcurrentMapCacheManager
│ - Redis, Memcached, EhCache
└─────────────────────────────────────┘
Включение кэширования
// Шаг 1: Добавить аннотацию на конфигурацию
@Configuration
@EnableCaching // Активирует кэширование
public class CacheConfig {
// По умолчанию использует SimpleCacheManager
}
// Шаг 2: Аннотировать методы
@Service
public class UserService {
@Cacheable("users") // Результат кэшируется
public User findById(Long id) {
System.out.println("Database query for: " + id);
return userRepository.findById(id).orElse(null);
}
}
// Использование
UserService service = context.getBean(UserService.class);
service.findById(1); // Вывод: "Database query for: 1" + БД запрос
service.findById(1); // Вывод: ничего (из кэша!)
service.findById(2); // Вывод: "Database query for: 2" + БД запрос
Основные аннотации
1. @Cacheable — читать из кэша
@Service
public class ProductService {
// Базовое использование
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// С кастомным ключом
@Cacheable(
value = "products",
key = "#id" // id из параметра
)
public Product getProductById(Long id) {
return productRepository.findById(id).orElse(null);
}
// С несколькими параметрами
@Cacheable(
value = "orders",
key = "#userId + ':' + #orderId" // Комбинированный ключ
)
public Order getOrder(Long userId, Long orderId) {
return orderRepository.findByUserIdAndId(userId, orderId);
}
// С условием
@Cacheable(
value = "products",
unless = "#result == null" // Не кэшируем null
)
public Product findProduct(String name) {
return productRepository.findByName(name);
}
// Условие для кэширования
@Cacheable(
value = "products",
condition = "#id > 0" // Кэшируем только если id > 0
)
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
}
// Как это работает
public Product getProduct(Long id) {
// 1. Проверка: есть ли значение в кэше для ключа id?
Object cached = cache.get(id);
if (cached != null) {
return cached; // Возвращаем из кэша
}
// 2. Если нет — выполняем метод
Product product = productRepository.findById(id).orElse(null);
// 3. Сохраняем в кэш
cache.put(id, product);
return product;
}
2. @CachePut — всегда выполнить и обновить кэш
@Service
public class UserService {
// @Cacheable — если есть в кэше, не выполняем
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// @CachePut — всегда выполняем и обновляем кэш
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
// Использование
User user = userService.getUser(1); // Из БД в кэш
user.setName("John");
userService.updateUser(user); // Обновляет БД и кэш
}
3. @CacheEvict — удалить из кэша
@Service
public class ProductService {
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// Удалить одну запись из кэша
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Удалить все из кэша
@CacheEvict(value = "products", allEntries = true)
public void refreshAllProducts() {
// Например, переиндексировать
}
// Несколько кэшей
@CacheEvict(value = {"products", "categories"}, allEntries = true)
public void clearAll() { }
}
Кастомный CacheManager
// Конфигурация с Redis
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // TTL: 10 минут
.serializeValuesWith(
Jackson2JsonRedisSerializationContext
.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
return RedisCacheManager.create(factory);
}
}
// Или с Caffeine (in-memory)
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "products");
cacheManager.setCaffeine(
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
);
return cacheManager;
}
}
Реальный пример: многоуровневое кэширование
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private CacheManager cacheManager;
// Получить заказ с кэшированием
@Cacheable(
value = "orders",
key = "#orderId",
condition = "#orderId > 0",
unless = "#result == null"
)
public Order getOrder(Long orderId) {
System.out.println("Fetching order from DB: " + orderId);
return orderRepository.findById(orderId)
.orElse(null);
}
// Обновить заказ и кэш
@CachePut(
value = "orders",
key = "#order.id"
)
public Order updateOrder(Order order) {
System.out.println("Updating order in DB: " + order.getId());
return orderRepository.save(order);
}
// Удалить заказ и кэш
@CacheEvict(
value = "orders",
key = "#orderId"
)
public void deleteOrder(Long orderId) {
System.out.println("Deleting order: " + orderId);
orderRepository.deleteById(orderId);
}
// Очистить весь кэш заказов
public void clearOrderCache() {
Cache cache = cacheManager.getCache("orders");
if (cache != null) {
cache.clear();
}
}
}
// Использование
OrderService service = context.getBean(OrderService.class);
service.getOrder(1); // "Fetching order from DB: 1"
service.getOrder(1); // (из кэша, ничего не печатается)
service.updateOrder(order); // "Updating order in DB: 1"
service.deleteOrder(1); // "Deleting order: 1"
Проблемы и решения
1. Проблема: методы на том же классе не кэшируются
@Service
public class UserService {
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
public void refreshUser(Long id) {
User user = getUser(id); // Кэширование НЕ РАБОТАЕТ!
// Потому что это вызов от non-proxied экземпляра
}
}
// Решение: инъектировать через конструктор
@Service
public class UserService {
private final UserRepository userRepository;
private final UserService self; // Самого себя, но через proxy
@Autowired
public UserService(UserRepository userRepository, UserService self) {
this.userRepository = userRepository;
this.self = self;
}
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
public void refreshUser(Long id) {
User user = self.getUser(id); // Через proxy — работает кэширование!
}
}
2. Проблема: кэширование null значений
// Плохо: кэширует null
@Cacheable("users")
public User findByEmail(String email) {
return userRepository.findByEmail(email); // Может вернуть null
}
// Хорошо: не кэширует null
@Cacheable(
value = "users",
unless = "#result == null" // Исключаем null
)
public User findByEmail(String email) {
return userRepository.findByEmail(email);
}
3. Проблема: TTL (Time To Live)
// Кэш на всегда (или до перезагрузки приложения)
// Решение: использовать @CacheEvict с scheduled
@Component
public class CacheScheduler {
@Scheduled(fixedDelay = 300000) // Каждые 5 минут
@CacheEvict(
value = "products",
allEntries = true
)
public void clearProductCache() {
System.out.println("Clearing product cache");
}
}
Сравнение реализаций
| CacheManager | Плюсы | Минусы |
|---|---|---|
| SimpleCacheManager | Просто, встроено | Память, нет TTL |
| ConcurrentMapCacheManager | Потокобезопасность | Память, нет TTL |
| EhCache | Дисковое хранилище | Конфигурация |
| Redis | Распределённо, быстро | Требует сервера |
| Caffeine | Быстро, TTL, eviction | Память |
Лучшие практики
-
Используй кэширование для читаемых, дорогих операций
@Cacheable("users") public User getUser(Long id) { } // Хорошо @Cacheable("updates") public void updateUser(User user) { } // Плохо! -
Планируй инвалидацию кэша
@CacheEvict(value = "users", key = "#id") public void updateUser(User user) { } -
Используй TTL
// В конфигурации Redis: entryTtl(Duration.ofMinutes(10)) -
Мониторь эффективность
// Смотри hit rate (сколько % запросов из кэша) -
Будь осторожен с синхронизацией
@Cacheable(sync = true) // Потокобезопасное кэширование public User getUser(Long id) { }