Ответ
Контекст: В высоконагруженном микросервисе обработки заказов возникла проблема периодических падений производительности и OutOfMemoryError в продакшене.
Анализ проблемы:
- С помощью профилировщика (VisualVM) и анализа дампов памяти выяснилось, что причиной является неограниченный рост in-memory кэша на основе
ConcurrentHashMap, хранившего историю сессий пользователей. - Объекты сессий никогда не удалялись из кэша, так как на них сохранялись ссылки из долгоживущих объектов.
Принятое решение и реализация:
Было решено заменить кэш на реализацию с TTL (Time-To-Live) и LRU (Least Recently Used) политикой вытеснения, используя Caffeine — высокопроизводительную библиотеку кэширования.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class SessionCache {
private final Cache<String, UserSession> cache;
public SessionCache() {
this.cache = Caffeine.newBuilder()
// Максимальный размер кэша
.maximumSize(10_000)
// Время жизни записи после создания
.expireAfterWrite(30, TimeUnit.MINUTES)
// Время жизни записи после последнего доступа
.expireAfterAccess(10, TimeUnit.MINUTES)
// Слабая ссылка на значения (позволяет GC удалить объект, если нет других ссылок)
.weakValues()
// Слушатель для отладки удаления
.removalListener((key, value, cause) ->
log.debug("Removed key: {} due to {}", key, cause))
.build();
}
public UserSession getSession(String sessionId) {
return cache.getIfPresent(sessionId);
}
public void putSession(String sessionId, UserSession session) {
cache.put(sessionId, session);
}
}
Результат:
- Потребление память стабилизировалось.
- Частота сборок мусора (GC) снизилась на ~40%.
- Производительность сервиса перестала деградировать со временем.
- Решение было задокументировано и внедрено в другие сервисы команды как стандартный паттерн для кэширования временных данных.