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

Как работает кэширование в Spring?

1.8 Middle🔥 141 комментариев
#Spring Framework

Комментарии (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Память

Лучшие практики

  1. Используй кэширование для читаемых, дорогих операций

    @Cacheable("users")
    public User getUser(Long id) { } // Хорошо
    
    @Cacheable("updates")
    public void updateUser(User user) { } // Плохо!
    
  2. Планируй инвалидацию кэша

    @CacheEvict(value = "users", key = "#id")
    public void updateUser(User user) { }
    
  3. Используй TTL

    // В конфигурации Redis: entryTtl(Duration.ofMinutes(10))
    
  4. Мониторь эффективность

    // Смотри hit rate (сколько % запросов из кэша)
    
  5. Будь осторожен с синхронизацией

    @Cacheable(sync = true) // Потокобезопасное кэширование
    public User getUser(Long id) { }