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

Какой знаешь самый простой способ реализации кэширования?

1.2 Junior🔥 251 комментариев
#Кэширование и NoSQL

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

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

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

# Самые простые способы реализации кэширования в Java

1. HashMap - Самый простой способ

Без каких-либо зависимостей, используя встроенные классы Java.

public class SimpleCacheExample {
    private static final Map<String, String> CACHE = new HashMap<>();
    
    public static String getData(String key) {
        // Проверяем, есть ли в кэше
        if (CACHE.containsKey(key)) {
            System.out.println("Из кэша: " + key);
            return CACHE.get(key);
        }
        
        // Если нет - получаем данные (дорогая операция)
        String value = fetchFromDatabase(key);
        
        // Сохраняем в кэш
        CACHE.put(key, value);
        return value;
    }
    
    private static String fetchFromDatabase(String key) {
        // Имитация долгого запроса к БД
        return "Данные для " + key;
    }
}

Проблемы простого HashMap

// ПРОБЛЕМА 1: Может расти бесконечно
// Решение: использовать LinkedHashMap с ограничением размера

public class LimitedCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_SIZE = 1000;
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > MAX_SIZE;  // Удаляет самую старую запись
    }
}

// Использование
Map<String, String> cache = new LimitedCache<>();
cache.put("key1", "value1");
cache.put("key2", "value2");

2. ConcurrentHashMap - Thread-Safe версия

Для многопоточных приложений используй ConcurrentHashMap:

public class ThreadSafeCache {
    private static final Map<String, String> CACHE = new ConcurrentHashMap<>();
    
    public String getData(String key) {
        // Потокобезопасно
        return CACHE.computeIfAbsent(key, k -> {
            System.out.println("Загружаю: " + k);
            return fetchFromDatabase(k);
        });
    }
    
    private String fetchFromDatabase(String key) {
        // Дорогая операция
        return "Данные для " + key;
    }
}

Преимущества computeIfAbsent:

  • Потокобезопасно
  • Вычисляет значение только если ключа нет
  • Компактный код

3. Spring @Cacheable - Самый удобный способ

Если используешь Spring, это самый простой способ.

Настройка

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@Configuration
@EnableCaching  // Включаем кэширование
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "products");
    }
}

Использование

@Service
public class UserService {
    @Cacheable("users")  // Кэширует результат
    public User getUser(Long id) {
        System.out.println("Запрос в БД для: " + id);
        return userRepository.findById(id).orElse(null);
    }
    
    @CacheEvict("users", allEntries = true)  // Очищает кэш
    public void clearUserCache() {}
    
    @CachePut("users")  // Обновляет кэш
    public User updateUser(Long id, User user) {
        return userRepository.save(user);
    }
}

С ключом кэша

@Service
public class ProductService {
    // Кэширует отдельно для каждого category
    @Cacheable(value = "products", key = "#category")
    public List<Product> getProductsByCategory(String category) {
        System.out.println("Загружаю товары категории: " + category);
        return productRepository.findByCategory(category);
    }
    
    // Более сложный ключ
    @Cacheable(value = "products", key = "#category + '-' + #page")
    public Page<Product> getProducts(String category, int page) {
        return productRepository.findByCategory(category, PageRequest.of(page, 10));
    }
}

4. Guava Cache - Функциональный способ

Библиотека Google Guava предоставляет элегантный Cache API.

<!-- pom.xml -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>
@Service
public class UserServiceWithGuava {
    private final LoadingCache<Long, User> userCache;
    
    public UserServiceWithGuava(UserRepository userRepository) {
        // Создаём кэш с автоматической загрузкой
        this.userCache = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)  // TTL: 10 минут
            .maximumSize(10000)                       // Максимум 10k элементов
            .build(
                new CacheLoader<Long, User>() {
                    @Override
                    public User load(Long id) throws Exception {
                        System.out.println("Загружаю user: " + id);
                        return userRepository.findById(id)
                            .orElseThrow(() -> new Exception("User not found"));
                    }
                }
            );
    }
    
    public User getUser(Long id) throws ExecutionException {
        return userCache.get(id);  // Автоматически загружает если нет в кэше
    }
    
    public void invalidateUser(Long id) {
        userCache.invalidate(id);  // Удаляет из кэша
    }
}

5. Redis - Для продакшена

Для распределённого кэширования используй Redis с Spring.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# application.properties
spring.redis.host=localhost
spring.redis.port=6379
spring.cache.type=redis
spring.cache.redis.time-to-live=600000  # 10 минут в миллисекундах
@Service
public class UserServiceWithRedis {
    @Autowired
    private RedisTemplate<String, User> redisTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    private static final String CACHE_KEY_PREFIX = "user:";
    
    public User getUser(Long id) {
        String cacheKey = CACHE_KEY_PREFIX + id;
        
        // Пытаемся получить из Redis
        User cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            System.out.println("Из Redis: " + id);
            return cached;
        }
        
        // Если нет - получаем из БД
        User user = userRepository.findById(id).orElse(null);
        
        if (user != null) {
            // Сохраняем в Redis на 10 минут
            redisTemplate.opsForValue().set(
                cacheKey, 
                user, 
                Duration.ofMinutes(10)
            );
        }
        
        return user;
    }
    
    public void clearUserCache(Long id) {
        redisTemplate.delete(CACHE_KEY_PREFIX + id);
    }
}

// ИЛИ с @Cacheable
@Service
public class UserServiceWithRedisAnnotations {
    @Cacheable(value = "users", key = "#id")
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Сравнение способов

СпособПростотаМасштабируемостьПотокобезопасностьКогда использовать
HashMap⭐⭐⭐⭐⭐НетПрототип, учёба
ConcurrentHashMap⭐⭐⭐⭐⭐⭐ДаНебольшие приложения
Spring @Cacheable⭐⭐⭐⭐⭐⭐ДаSpring приложения
Guava Cache⭐⭐⭐⭐⭐⭐ДаНужна автоматическая загрузка
Redis⭐⭐⭐⭐⭐⭐⭐ДаПродакшен, микросервисы

Практический пример: от простого к продакшену

Шаг 1: Прототип

public class UserService {
    private Map<Long, User> cache = new ConcurrentHashMap<>();
    
    public User getUser(Long id) {
        return cache.computeIfAbsent(id, key -> loadFromDb(key));
    }
    
    private User loadFromDb(Long id) {
        // ...
    }
}

Шаг 2: Spring приложение

@Service
public class UserService {
    @Cacheable("users")
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Шаг 3: Продакшен с Redis

@Service
public class UserService {
    @Cacheable(value = "users", key = "#id", cacheManager = "redisManager")
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Типичные ошибки

// ОШИБКА 1: Кэширование без TTL (время жизни)
// Проблема: кэш может содержать устаревшие данные
// Решение: задай expireAfterWrite
CacheBuilder.newBuilder()
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build();

// ОШИБКА 2: Кэширование null значений
// Проблема: можешь кэшировать отсутствие данных
// Решение: проверяй перед кэшированием
if (value != null) {
    cache.put(key, value);
}

// ОШИБКА 3: Игнорирование инвалидации кэша
// Проблема: после обновления данных кэш содержит старые значения
// Решение: очищай кэш при обновлении
@CacheEvict("users", key = "#id")
public void updateUser(Long id, User user) {
    // ...
}

Вывод

Самый простой способ - использовать Spring @Cacheable, потому что:

  1. Минимум кода
  2. Потокобезопасно
  3. Легко переключиться с памяти на Redis
  4. Не нужно писать getter/setter для кэша
  5. Работает с любыми типами данных
Какой знаешь самый простой способ реализации кэширования? | PrepBro