Пример задачи:У нас есть сайт по учёту павлинов всего мира. Он неожиданно для всех стал безумно популярным и мы хотим масштабироваться.Мы храним информацию о павлинах в PostgreSQL в таблице peacock:Павлины в табличке peacock* Новые павлины рождаются примерно 1 раз в секунду.* Различные запросы по этой таблице выполняются около 100 раз в секунду.Наш PostgreSQL спокойно выдерживает такую нагрузку.Но тут мы решили добавить на главную страницу сайта диаграмму распределения павлинов по зонам в стране пользователя в реальном времени.Очень важная диаграмма распределения павлинов на главной страницеГлавная страница открывается 2000 раз в секунду. Наш PostgreSQL уже почти не выдерживает выполнение такого запроса с такой частотой:select zone_id, count(*) from peacock where zone_id in (1,2,3) group by zone_id;Нагрузка на бэкенд и БДПоэтому мы решаем использовать кэшированиеКэширование in-memoryПопробуем сохранить результат запроса в память нашего веб-сервера.Для этого будем использовать Guava CacheBuilder. Это очень гибкая реализация кэша в памяти от google.Gauva Cache in-memoryТак будет выглядеть инициализация кэша:Инициализация guava кэшаА так мы будем доставать из него данные для диаграммы:Прочитать из кэша количество павлинов в зонах 1, 2 и 3Что же тут происходит:1. Кэш будет возвращать нам количество павлинов по zoneId [zoneId -> peackockCount]2. Если подсунуть ему список zoneId, он вернет Map [zoneId -> peackockCount]3. Мы ограничили максимальный размер зон до 1000. Если мы попытаемся добавить 1001 зону, кэш сам выкинет зону, которую дольше всего не запрашивали (смотри LRU-cache)4. Мы научили кэш самостоятельно подгружать количество павлинов в зоне, если таких данных еще нетЧтобы это работало правильно осталось только инвалидировать зону в кэше, когда рождается новый павлиненок или павлин переезжает в другую зону:Инвалидация guava кэшаТогда при следующем запросе кэш не найдет данных по этой зоне и сам сходит в БД.Таким образом, 2000 запросов/сек будут попадать в кэш, а БД будет спокойно заниматься своими делами. И только лишь раз в секунду одна из зон будет инвалидироваться, вызывая одно чтение из БД.Все счастливы!Почти...Кэширование в RedisИдет время. 2000 запросов с главной страницы плавно превращаются в 4000. Мы понимаем, что наш бэкенд перестает успевать справляться с нагрузкой, и решаем масштабироваться горизонтально - добавлением инстансов бэкендов.Смасштабировались до трех бэкендовТеперь запрос от клиента может попасть в один из 3 инстансов бэкенда.Вопрос с нагрузкой решен, но наше кэширование сломалось!При добавлении нового павлина инвалидация будет происходить только в том бэкенде, который обрабатывал запрос добавления. В остальных же бэкендах число павлинов останется неактуальным:Проблема с инвалидацией параллельных in-memory кэшейОдним из выходов из этой ситуации будет создание общего кэша, который будет находится вне бэкендов:Кэширование павлинов в RedisТеперь все закэшированные данные лежат в одном месте. Поэтому инвалидация происходит корректно.Минусы этого подхода по сравнению с in-memory:- еще один сервер, который нужно поднять и поддерживать в рабочем состоянии (пока кэш лежит, базе будет очень туго)- каждый запрос в кэш теперь = поход по сети (это намного дороже, чем достать из памяти)Плюсы:- эффективность кэша значительно больше в случае с несколькими инстансами бэкенда (чтобы сохранить в память 3-х бэкендов значение одного ключа, его нужно посчитать 3 раза)- можно запихнуть намного больше данных (redis из коробки умеет шардироваться и реплицироваться = независимое горизонтальное масштабирование кэша)- единое место для инвалидации (как в примере выше)- мы делаем намного приятнее нашему GC (содержать кучу памяти для кэша в old gen не так бесплатно как кажется)- redis еще и очень быстрый, потому что хранит все данные в памяти- redis поддерживает очень много полезных операций с данными (например, incr в нашем примере мог бы помочь вообще избежать инвалидации)Альтернативные технологииIn-memory: CaffeineDistributed cache: Hazelcast, Apache IgniteВыводы1. Кэширование может значительно уменьшить время ответа на запросы и нагрузку на основной источник данных.2. Нельзя однозначно выбрать, где держать кэш лучше. Если есть только один инстанс бэкенда и кэш маленький, то лучше брать in-memory. Если бэкендов много, и кэша много, и нужна явная инвалидация, то лучше брать Redis.