Как стать автором
Обновить

Комментарии 21

Решал похожую задачу.

У меня был один TTL, только логика немного другая: TTL не влияет на выдачу (на запрос всегда отдаётся кэшированное даже устаревшее значение).
Однако, есть фоновая команда, которая периодически проверяет устаревшие значения и обновляет их. Так что фактический максимальный TTL = указанный TTL + интервал обновления + скорость обработки.
Ленивость и экономия памяти обеспечивалась логином: если за некоторый период юзер не логинился, то его кэш очищается.
(Может, для этой логики тоже есть термин?)

В итоге: нет проблемы «первого запроса» для активных пользователей, нагрузка фоновая, нагрузка равномерна (размазана по времени и не создаёт резких пиков), при нехватки памяти нагрузка не увеличится (просто реальный TTL будет меньше того что в конфиге).
Согласен, тоже хорошее решение. Я пробовал фоновый поток, обходящий кеши. Но очень быстро вышло, что слишком много надо было пересчитывать. Когда пришлось ввести 2 фоновых потока, постоянно без пауз что-то считающих, я пошел «в другую сторону».
Не совсем понял, получается вы в фоне проверяли актуальность всех кешей?
В этом случае есть плюсы, т.к. всегда есть актуальный кеш.
Но с другой стороны, может быть такой кейс:
каталог товаров из 1 млн. (вместе с торговыми предложениями) позиций, вы постоянно актуализируете кеш этих товаров, например какие-то характеристики, но по сути, из всего каталога, у вас 99% запросов приходится только на 30% каталога, остальные — тухляк.
Получается, что в вы актуализируете постоянно кеш данных, к которым идут запросы очень редко.
Совершенно верно. И придя к тем же выводам, я убрал «фоновую актуализацию» и внедрил «2-х диапазонные кеши», которые обновляются только в том случае, если востребованы.
> Получается, что в вы актуализируете постоянно кеш данных, к которым идут запросы очень редко.

Нет. Цитирую:«Ленивость и экономия памяти обеспечивалась логином: если за некоторый период юзер не логинился, то его кэш очищается.».

Это не серебряная пуля, я топлю за анализ поведения системы перед тем как его менять (-:
В моём случае я знаю что юзеров (которые логинились хотя бы раз за последние два дня) в среднем около 30% и держать данные для них в кэше выгодно по этому кртерию. Возможно, в другой системе при других условиях я бы выбирал другой критерий.
случае нескольких одновременных обращений к записи с TTL1 < t < TTL2, в очереди будет находиться только 1 задача на обновление, а не несколько одинаковых.


Здесь не понял как и кто за этим следит. Все параллельные процессы инициализируют обновление кэша, так-как для каждого из них выполняется условие TTL1 < t < TTL2. И каждый процесс запустит эвент «кэш мне запили». Очередь, конечно, можно разобрать с условием, что есть только один подписчик, который обновляет кэш, и второе аналогичное задание он пропустит так-как t станет < TTL1 и < TTL2 но что если подписчиков больше одного? В общем, сама по себе концепция не дает возможности предотвратить двойное обновление кэша.

Плюс я не совсем уверен, что нужен второй TTL как таковой. Если есть доступ к значению времени истечения актуальности кэша, ставим сразу значение второго TTL, как основное, и при запросе пользователем данных, отдаем кэш, и проверяем сколько прошло времени. Если уже порядочно, то кидаем эвент на обновление кэша.
Да, концепция не дает. Но указанную проблему мы решили. Дело в том, что задачи на обновление у нас идут через Rabbit. Соответственно, поток, занимающийся пересчетом, получая задачу, просто проверяет наличие в кеше соответствующего значения с t < TTL1. Если так и есть, значит задача — дубль, пропускаем. Если t > TTL1 — работаем. И есть отдельный поток — он занимается принудительным пересчетом (иногда надо обновить срочно и вне общей очереди).
PS На самом деле немного обманул — дополнительно еще ставится/проверяется/удаляется спец. флаг, означающий «задача есть в очереди». Для этого мы используем Redis.

По второму замечанию — тут сложнее определять, что значит «порядочно» в каждом конкретном случае. Фиксация в секундах или процентах наверняка где-то будет препядствием. Да и в плане разработки — посложнее. При появлении нового набора данных для кеширования, надо внести правки и в общую процедуру «дай кеш-значение».
Флаг в редисе это потенциальный дедлок. Сообщение может пропасть, консюмер может упасть и некому будет удалить флаг.

Если у вас мемкеш, посмотрите в сторону cas.
О, с редисом все нормально — ставим довольно короткий TTL для записи самого флага, так что дедлоков не случается. Да и новый консюмер поднимается в течение 10 секунд максимум, если что.
А вот на счет мемкеш — спасибо, учтем.

Похоже на адский велосипед, в котором ещё и очередь сообщений задействована. Если вам нужно сгенерировать запись в кеш, а остальные читатели должны подождать — rw lock вам в руки.

Идея как раз в том, чтобы по-возможности вообще никогда не заставлять ждать (самое первое генерирование не в счет). А большая «навороченность» обусловлена далеко не столько идеей 2-диапазонного кеширования, сколько размером системы — у много-серверных продуктов свои особенности и требования.

Так тоже можно, вам нужен распределённый лок, вещь очень частая в распределённых системах.


  1. Вам нужен Redis и операция compare and set. Делается в виде lua скрипта.
  2. Запрос читает данные из редиса и решает нужно ли их обновлять. Например данные истекли или их нет.
  3. Если нужно обновить и compareAndSet(key, busy, null) == null то загрузить и обновить. В зависимости от TTL либо в фоне либо заблокировать процесс пока загрузка не кончится.

Предусмотреть логику для перезапуска процесса обновления если он завис или не завершился. В идеале с помощью механизма устаревания ключей редиса. Ключ key умирает и следующий compareAndSet загрузит данные.


Вместо Redis можно БД использовать как хранилище распределённых локов.

Не совсем. Подобную технологию я использовал ранее. Ее цель — убрать случаи пиковой нагрузки.
Например, если на странице нужны 10 кеш-значений, с TTL=100, то по истечении этих 100 секунд все 10 блоков будут перегенерироваться в рамках одного запроса. Чтобы этого избежать, у меня автоматом добавлялась вариативность времен в пределах +-20%. Что гарантировало «постепенное» обновление всего набора.
Получается, что идеи хоть и очень схожи, но все-таки разные. Моя основная цель — «ленивое» обновление, что как часть включает в себя и «защиту сервера».

Я так понял, вашем случае защиты сервера нет. Когда одновременно прилетает 100500 запросов на одно кеш-значение, да, вы быстро отдадите пользователю ответ, но при этом 100499 раз избыточно перегенерируете кеш.

Отчего ж? Аж двойная.
1. Спец. флаг в Redis (защищает при первом генерировании в основном)
2. Контрольная проверка наличия кеш-значения с t < TTL1 в начале процедуры перегенерации. Это если очередь долго разгребалась.
  1. Я все-таки в уме держу некоторую абстракцию, где кеш может быть не только в redis, где такого флага может не быть
  2. И вот как раз в ситуации большого количества запросов TTL1 < t < TTL2, когда запрос на перегенерацию уже отправлен, но еще не выполнен, есть вероятность получить много таких холостых перегенераций
Да, видимо оно самое. Было бы странно, если бы такую, в общем-то несложную, вещь я бы придумал первым. Но тем не менее, информации на русском — найти не удалось, на английском — очень сложно (и то только благодаря комментариям к этой статье).

Добрый день!
Спасибо за статью!


Подскажите, а на сколько надёжен ваш описанный кейс по поводу актуализации кеша через реббит + проверка на существование таски через редис?
Практики с очень высокими нагрузками не было. Хотел бы взять на вооружение.

Пока проблем не было. Посещаемость у нас — ~10 млн обращений к скриптам динамики в месяц. Половина из обращений так или иначе работает с кеш-записями.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории