Комментарии 21
У меня был один TTL, только логика немного другая: TTL не влияет на выдачу (на запрос всегда отдаётся кэшированное даже устаревшее значение).
Однако, есть фоновая команда, которая периодически проверяет устаревшие значения и обновляет их. Так что фактический максимальный TTL = указанный TTL + интервал обновления + скорость обработки.
Ленивость и экономия памяти обеспечивалась логином: если за некоторый период юзер не логинился, то его кэш очищается.
(Может, для этой логики тоже есть термин?)
В итоге: нет проблемы «первого запроса» для активных пользователей, нагрузка фоновая, нагрузка равномерна (размазана по времени и не создаёт резких пиков), при нехватки памяти нагрузка не увеличится (просто реальный TTL будет меньше того что в конфиге).
В этом случае есть плюсы, т.к. всегда есть актуальный кеш.
Но с другой стороны, может быть такой кейс:
каталог товаров из 1 млн. (вместе с торговыми предложениями) позиций, вы постоянно актуализируете кеш этих товаров, например какие-то характеристики, но по сути, из всего каталога, у вас 99% запросов приходится только на 30% каталога, остальные — тухляк.
Получается, что в вы актуализируете постоянно кеш данных, к которым идут запросы очень редко.
Нет. Цитирую:«Ленивость и экономия памяти обеспечивалась логином: если за некоторый период юзер не логинился, то его кэш очищается.».
Это не серебряная пуля, я топлю за анализ поведения системы перед тем как его менять (-:
В моём случае я знаю что юзеров (которые логинились хотя бы раз за последние два дня) в среднем около 30% и держать данные для них в кэше выгодно по этому кртерию. Возможно, в другой системе при других условиях я бы выбирал другой критерий.
случае нескольких одновременных обращений к записи с TTL1 < t < TTL2, в очереди будет находиться только 1 задача на обновление, а не несколько одинаковых.
Здесь не понял как и кто за этим следит. Все параллельные процессы инициализируют обновление кэша, так-как для каждого из них выполняется условие TTL1 < t < TTL2. И каждый процесс запустит эвент «кэш мне запили». Очередь, конечно, можно разобрать с условием, что есть только один подписчик, который обновляет кэш, и второе аналогичное задание он пропустит так-как t станет < TTL1 и < TTL2 но что если подписчиков больше одного? В общем, сама по себе концепция не дает возможности предотвратить двойное обновление кэша.
Плюс я не совсем уверен, что нужен второй TTL как таковой. Если есть доступ к значению времени истечения актуальности кэша, ставим сразу значение второго TTL, как основное, и при запросе пользователем данных, отдаем кэш, и проверяем сколько прошло времени. Если уже порядочно, то кидаем эвент на обновление кэша.
PS На самом деле немного обманул — дополнительно еще ставится/проверяется/удаляется спец. флаг, означающий «задача есть в очереди». Для этого мы используем Redis.
По второму замечанию — тут сложнее определять, что значит «порядочно» в каждом конкретном случае. Фиксация в секундах или процентах наверняка где-то будет препядствием. Да и в плане разработки — посложнее. При появлении нового набора данных для кеширования, надо внести правки и в общую процедуру «дай кеш-значение».
Если у вас мемкеш, посмотрите в сторону cas.
Похоже на адский велосипед, в котором ещё и очередь сообщений задействована. Если вам нужно сгенерировать запись в кеш, а остальные читатели должны подождать — rw lock вам в руки.
Так тоже можно, вам нужен распределённый лок, вещь очень частая в распределённых системах.
- Вам нужен Redis и операция compare and set. Делается в виде lua скрипта.
- Запрос читает данные из редиса и решает нужно ли их обновлять. Например данные истекли или их нет.
- Если нужно обновить и compareAndSet(key, busy, null) == null то загрузить и обновить. В зависимости от TTL либо в фоне либо заблокировать процесс пока загрузка не кончится.
Предусмотреть логику для перезапуска процесса обновления если он завис или не завершился. В идеале с помощью механизма устаревания ключей редиса. Ключ key умирает и следующий compareAndSet загрузит данные.
Вместо Redis можно БД использовать как хранилище распределённых локов.
Ну и дальше гуглить решения и алгоритмы этого вопроса.
Будут те самые готовые решения вашего вопроса.
symfony.com/blog/new-in-symfony-4-2-cache-stampede-protection
Например, если на странице нужны 10 кеш-значений, с TTL=100, то по истечении этих 100 секунд все 10 блоков будут перегенерироваться в рамках одного запроса. Чтобы этого избежать, у меня автоматом добавлялась вариативность времен в пределах +-20%. Что гарантировало «постепенное» обновление всего набора.
Получается, что идеи хоть и очень схожи, но все-таки разные. Моя основная цель — «ленивое» обновление, что как часть включает в себя и «защиту сервера».
Я так понял, вашем случае защиты сервера нет. Когда одновременно прилетает 100500 запросов на одно кеш-значение, да, вы быстро отдадите пользователю ответ, но при этом 100499 раз избыточно перегенерируете кеш.
1. Спец. флаг в Redis (защищает при первом генерировании в основном)
2. Контрольная проверка наличия кеш-значения с t < TTL1 в начале процедуры перегенерации. Это если очередь долго разгребалась.
- Я все-таки в уме держу некоторую абстракцию, где кеш может быть не только в redis, где такого флага может не быть
- И вот как раз в ситуации большого количества запросов TTL1 < t < TTL2, когда запрос на перегенерацию уже отправлен, но еще не выполнен, есть вероятность получить много таких холостых перегенераций
Похоже, вы изобрели stale-while-revalidate (например, как здесь https://tools.ietf.org/html/rfc5861#section-3 )
Для Node.js мы иcпользовали https://github.com/thomheymann/stale-lru-cache
Добрый день!
Спасибо за статью!
Подскажите, а на сколько надёжен ваш описанный кейс по поводу актуализации кеша через реббит + проверка на существование таски через редис?
Практики с очень высокими нагрузками не было. Хотел бы взять на вооружение.
2R2L кеширование