Ответ
Локальный (in-memory) кэш хранит данные в оперативной памяти экземпляра сервиса. Его основная цель — резкое снижение задержки (latency) и нагрузки на нижележащие источники данных (базы данных, другие сервисы) для часто запрашиваемой, относительно статичной информации.
Цели и преимущества:
- Минимальная задержка: Доступ к данным из памяти на порядки быстрее, чем сетевой вызов или запрос к БД.
- Снижение нагрузки: Уменьшает количество запросов к БД или внешним API, защищая их от перегрузки.
- Устойчивость к сбоям: При недоступности основного источника данных сервис может какое-то время работать с устаревшими (кэшированными) данными.
- Простота: Легко реализуется с помощью библиотек (Caffeine, Ehcache) или фреймворков (Spring Cache).
Пример с Spring Cache и Caffeine:
@Service
public class CatalogService {
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
// Дорогой вызов к БД выполняется только при промахе кэша
return productRepository.findById(id).orElseThrow();
}
}
// Конфигурация Caffeine для TTL и размера
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return manager;
}
}
Компромиссы и проблемы:
- Несогласованность данных (Consistency): Каждый экземпляр сервиса имеет свою копию кэша. Изменение данных в одном экземпляре не обновляет кэш в других. Это проблема для данных, требующих сильной согласованности.
- Использование памяти: Кэш потребляет RAM, что ограничивает его объем и требует стратегий вытеснения (LRU, LFU).
- Сложность инвалидации: Актуальность данных зависит от TTL (время жизни) или явной инвалидации при обновлении.
Когда использовать: Для данных, которые часто читаются, редко меняются и допускают eventual consistency (справочники, конфигурация, статичный контент). Для синхронизации между инстансами или часто изменяющихся данных часто используют распределенный кэш (Redis, Hazelcast) поверх или вместо локального.
Ответ 18+ 🔞
Ну слушай, вот есть у тебя сервис, а в нём данные. И каждый раз, когда кто-то эти данные просит, ты лезешь в базу, которая где-то там, за хуйней сетевой, и тащишь их. А потом ещё раз лезешь, потому что другой запрос пришёл. И так до тех пор, пока база не скажет: «Да иди ты нахуй, я устала!» — и ляжет.
Так вот, чтобы этого не происходило, умные люди придумали локальный кэш. Суть проста, как три рубля: ты берёшь самые популярные, часто запрашиваемые данные — и просто запихиваешь их в оперативную память своего сервиса. Всё. Больше никуда ходить не надо. Запросили продукт с ID 123? Бдыщ! Он уже тут, в памяти, готовый. Задержка — микросекунды, а не миллисекунды. База отдыхает, все довольны. Просто ёбушки-воробушки!
Зачем это, собственно, надо:
- Скорость, блядь! Достать что-то из памяти своего же процесса — это на порядки быстрее, чем тащиться через сеть до базы или другого сервиса. Прям волшебство, хуле.
- Разгрузить основную тягловую силу. База данных — она как та лошадь: может много, но если её постоянно погонять, сдохнет. Кэш принимает на себя львиную долю простых читающих запросов, и база не захлёбывается.
- Немного живучести. Если база вдруг легла, сервис какое-то время может протянуть на старых, закэшированных данных. Не на всех, конечно, но хоть что-то будет работать, а не просто «ошибка 500».
- Проще некуда. Взял какую-нибудь библиотечку вроде Caffeine, накропал пару аннотаций — и всё, кэш готов. Не надо городить свою велосипедную телегу с мапами и таймерами.
Вот, смотри, как это выглядит в коде (Spring + Caffeine):
@Service
public class CatalogService {
// Говорим: «Слушай, Spring, перед тем как лезть в метод, проверь кэш с именем "products"»
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
// А вот эта дорогая операция — запрос к БД — выполнится ТОЛЬКО если в кэше пусто.
return productRepository.findById(id).orElseThrow();
}
}
// А тут мы настраиваем, как этот кэш будет себя вести
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
// Данные протухнут через 10 минут после записи
.expireAfterWrite(10, TimeUnit.MINUTES)
// И больше 1000 записей держать не будем — память не резиновая
.maximumSize(1000));
return manager;
}
}
Но не всё так гладко, конечно. Есть и подводные, блядь, камни:
- Консистентность — её нет. Это главная проблема. У каждого экземпляра твоего сервиса — своя собственная копия кэша в памяти. Обновил данные в одном месте — в других-то кэшах осталась старая версия. Для данных, которые должны быть строго актуальными везде и сразу — это пиздец, а не решение.
- Жрёт память. Оперативка — ресурс конечный. Нельзя туда запихнуть всю базу. Приходится выбирать: что кэшировать, а что нет, и выкидывать старое по каким-то правилам (LRU, LFU).
- Инвалидация — головная боль. Когда данные всё-таки поменялись, надо как-то сказать кэшу: «Эй, дружок, это уже неактуально, выкинь». Либо ждать, пока само протухнет (TTL), либо городить механизмы ручного сброса.
Так когда же это юзать? Идеальный случай — данные, которые очень часто читаются, но почти никогда не меняются. Ну, всякие справочники, конфигурация, статичный контент. Там, где можно смириться с тем, что обновление придёт не мгновенно (eventual consistency). Если же данные меняются часто и должны быть синхронизированы между всеми экземплярами — тут уже нужен распределённый кэш (типа Redis), чтобы у всех была одна правда. Но это уже совсем другая история, с другими граблями.