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

Как реализовать глобальный кэш?

2.8 Senior🔥 91 комментариев
#Безопасность

Комментарии (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Учебные примеры
ConcurrentHashMapThread-safeНет TTLНебольшой кэш
TTL CacheTTLРучное управлениеСреднее приложение
LRU CacheОграничение памятиМедленнееФиксированный размер
Spring CacheДекларативныйЗависит от configProduction
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) лучше для многосерверного приложения
Как реализовать глобальный кэш? | PrepBro