Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализовать глобальный кэш в Java
Глобальный кэш — это хранилище часто используемых данных в памяти приложения для быстрого доступа. Вот несколько подходов от простого к сложному.
1. Простой глобальный кэш на базе HashMap
import java.util.HashMap;
import java.util.Map;
// Самый простой вариант
public class SimpleCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public void put(K key, V value) {
cache.put(key, value);
}
public V get(K key) {
return cache.get(key);
}
public boolean contains(K key) {
return cache.containsKey(key);
}
public void remove(K key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
}
// Использование
public class UserService {
private final SimpleCache<Long, User> userCache = new SimpleCache<>();
public User getUser(Long id) {
// Проверяем кэш
if (userCache.contains(id)) {
return userCache.get(id); // Из кэша
}
// Если нет в кэше - берем из БД
User user = database.findUserById(id);
userCache.put(id, user); // Сохраняем в кэш
return user;
}
}
Проблемы этого подхода:
- ❌ Не thread-safe (если несколько потоков используют одновременно)
- ❌ Нет TTL (время жизни кэша)
- ❌ Может расти бесконечно (утечка памяти)
- ❌ Нет приоритета вытеснения старых данных
2. Thread-safe кэш с ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentCache<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();
public void put(K key, V value) {
cache.put(key, value);
}
public V get(K key) {
return cache.get(key);
}
public V getOrDefault(K key, V defaultValue) {
return cache.getOrDefault(key, defaultValue);
}
public V computeIfAbsent(K key, java.util.function.Function<K, V> loader) {
return cache.computeIfAbsent(key, loader);
}
public void remove(K key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
public int size() {
return cache.size();
}
}
// Использование с автозагрузкой
public class UserService {
private final ConcurrentCache<Long, User> userCache = new ConcurrentCache<>();
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
// Загружает из кэша, или если нет - вызывает функцию
return userCache.computeIfAbsent(id, userId ->
userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId))
);
}
public void invalidateUser(Long id) {
userCache.remove(id);
}
}
Улучшения:
- ✅ Thread-safe
- ✅ computeIfAbsent - избегает дублирования загрузки
- ✅ Нет ненужных операций в synchronized блоках
Остаются проблемы:
- ❌ Нет TTL (время жизни)
- ❌ Неограниченный рост
3. Кэш с TTL (Time To Live)
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class CacheEntry<V> {
private final V value;
private final long expiryTime;
public CacheEntry(V value, long ttlMillis) {
this.value = value;
this.expiryTime = System.currentTimeMillis() + ttlMillis;
}
public boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
public V getValue() {
return value;
}
}
public class TTLCache<K, V> {
private final Map<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
private final long defaultTtlMillis;
public TTLCache(long defaultTtlMillis) {
this.defaultTtlMillis = defaultTtlMillis;
}
public void put(K key, V value) {
cache.put(key, new CacheEntry<>(value, defaultTtlMillis));
}
public void put(K key, V value, long ttlMillis) {
cache.put(key, new CacheEntry<>(value, ttlMillis));
}
public V get(K key) {
CacheEntry<V> entry = cache.get(key);
if (entry == null) {
return null; // Нет в кэше
}
if (entry.isExpired()) {
cache.remove(key); // Истекло
return null;
}
return entry.getValue();
}
public void remove(K key) {
cache.remove(key);
}
public void clear() {
cache.clear();
}
}
// Использование
public class UserService {
private final TTLCache<Long, User> userCache;
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
// TTL = 5 минут
this.userCache = new TTLCache<>(5 * 60 * 1000);
}
public User getUser(Long id) {
User cached = userCache.get(id);
if (cached != null) {
return cached; // Из кэша (если не истек)
}
// Загружаем из БД
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// Сохраняем в кэш на 5 минут (или специальный TTL)
userCache.put(id, user, 10 * 60 * 1000); // 10 минут для VIP
return user;
}
}
4. LRU кэш (Least Recently Used) - с ограничением размера
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> {
private final int maxSize;
private final Map<K, V> cache;
public LRUCache(int maxSize) {
this.maxSize = maxSize;
// LinkedHashMap с accessOrder=true отслеживает порядок доступа
this.cache = new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// Удаляем самый старый элемент, если размер превышен
return size() > maxSize;
}
};
}
public synchronized void put(K key, V value) {
cache.put(key, value);
}
public synchronized V get(K key) {
return cache.get(key);
}
public synchronized void remove(K key) {
cache.remove(key);
}
public synchronized void clear() {
cache.clear();
}
public synchronized int size() {
return cache.size();
}
}
// Использование
public class UserService {
private final LRUCache<Long, User> userCache;
public UserService() {
// Кэш максимум 1000 пользователей
this.userCache = new LRUCache<>(1000);
}
public User getUser(Long id) {
User cached = userCache.get(id);
if (cached != null) {
return cached;
}
User user = database.findUserById(id);
userCache.put(id, user); // Добавляет, старое удаляется если > 1000
return user;
}
}
5. Использование Spring Cache аннотаций (Рекомендуется)
// Зависимость в 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 {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// @Cacheable - если есть в кэше, вернуть из кэша
// если нет - вызвать метод и сохранить результат
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// @CachePut - всегда вызвать метод и обновить кэш
@CachePut(value = "users", key = "#result.id")
public User updateUser(User user) {
return userRepository.save(user);
}
// @CacheEvict - удалить из кэша
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// @CacheEvict с allEntries=true - очистить весь кэш
@CacheEvict(value = "users", allEntries = true)
public void rebuildCache() {
// Перестраиваем кэш
}
}
6. Redis как глобальный кэш (Production)
// Зависимость
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-data-redis</artifactId>
// </dependency>
@Configuration
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // TTL 10 минут
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
return RedisCacheManager.create(factory);
}
}
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
String key = "user:" + id;
// Проверяем Redis
User cached = (User) redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
// Загружаем из БД
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// Сохраняем в Redis на 10 минут
redisTemplate.opsForValue().set(
key,
user,
Duration.ofMinutes(10)
);
return user;
}
}
7. Сравнение подходов
| Подход | Преимущества | Недостатки | Используется для |
|---|---|---|---|
| HashMap | Простой | Не thread-safe | Учебные примеры |
| ConcurrentHashMap | Thread-safe | Нет TTL | Небольшой кэш |
| TTL Cache | TTL | Ручное управление | Среднее приложение |
| LRU Cache | Ограничение памяти | Медленнее | Фиксированный размер |
| Spring Cache | Декларативный | Зависит от config | Production |
| Redis | Распределённый, быстрый | Внешний сервис | Масштабируемое приложение |
8. Best Practices
✅ Используйте ключи с префиксом:
String key = "user:" + id; // "user:123"
String key = "post:comments:" + postId; // "post:comments:456"
✅ Устанавливайте разумный TTL:
// Данные профиля пользователя - 1 час
redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
// Данные, часто меняющиеся - 5 минут
redisTemplate.opsForValue().set(key, data, Duration.ofMinutes(5));
✅ Инвалидируйте кэш при обновлении:
public User updateUser(User user) {
User updated = userRepository.save(user);
redisTemplate.delete("user:" + user.getId()); // Удаляем из кэша
return updated;
}
❌ Избегайте кэширования sensitive данных:
// Плохо - не кэшировать пароли!
@Cacheable("users:passwords")
public String getPassword(Long userId) { }
Заключение
Для простых случаев: используйте ConcurrentHashMap
Для среднего приложения: Spring Cache с аннотациями
Для production: Redis (масштабируемо, надежно, быстро)
Всегда помните:
- Кэш должен иметь стратегию инвалидации
- TTL предотвращает устаревшие данные
- Распределённый кэш (Redis) лучше для многосерверного приложения