ЭФФЕКТИВНОСТЬ РАЗЛИЧНЫХ СТРАТЕГИЙ КЭШИРОВАНИЯ В ВЫСОКОНАГРУЖЕННЫХ ПРИЛОЖЕНИЯХ Северо-Кавказский федеральный университет, г. Ставрополь, Россия Аннотация. В работе исследуется, насколько разные способы организации промежуточного хранения данных помогают (и когда мешают) сервисам, которые ежедневно обрабатывают большой поток обращений. Автор исходит из того, что ускорение отклика само по себе ещё не доказывает удачность решения: важно одновременно смотреть на хвост задержек, долю запросов, дошедших до основной базы, и частоту ситуаций, когда пользователь получает уже неактуальную информацию. Сопоставляются четыре распространённых схемы согласования кэша и хранилища, а также связка локальной памяти процесса с сетевым хранилищем ключ–значение. Эксперименты выполнены на трёх прикладных стендах, близких к витрине, к API учёта заказов и к сервису тяжёлых выборок. Показано, что наибольший выигрыш даёт не «самый быстрый» движок, а заранее выбранная политика обновления для каждого класса данных. Отдельно разобраны сбои, возникающие при одновременном истечении популярных ключей, при одинаковом сроке жизни большой пачки записей и при потоке обращений к несуществующим идентификаторам. По итогам предложен набор рабочих правил, которые позволяют держать кэш управляемым, а не превращать его в скрытый источник расхождений. Ключевые слова: промежуточное хранение данных, высокая нагрузка, согласование кэша и базы, коэффициент попаданий, срок жизни записи, многослойная схема, отказоустойчивость чтения. Введение Сервисы, с которыми пользователь встречается каждый день — витрины, личные кабинеты, ленты, справочные порталы — давно вышли за рамки «нескольких сотен запросов в минуту». Даже относительно скромное по меркам отрасли приложение в часы пик обслуживает тысячи параллельных обращений, причём одно действие клиента часто тянет за собой цепочку чтений из разных таблиц и соседних сервисов. Повторяемость этих чтений очень высока: карточка товара, тариф, профиль, список статусов запрашиваются многократно, хотя меняются гораздо реже. Если каждый такой запрос каждый раз идёт на диск основной СУБД, система начинает деградировать не из ‑ за «плохой бизнес ‑ логики», а из ‑ за однообразной, предсказуемой и дорогой работы с данными. К середине текущего десятилетия сложилась ситуация, в которой прямой доступ к базе на каждое чтение для публичного контура уже не рассматривается как нормальная рабочая схема. Пользователь сравнивает скорость не с вчерашней версией того же сайта, а с самыми быстрыми сервисами, которыми он пользуется параллельно. Задержка в доли секунды на карточке товара или на обновлении корзины воспринимается как сбой. Одновременно растёт цена масштабирования реляционных хранилищ: добавить память дешевле, чем размножать узлы базы и разбираться с репликацией, блокировками и ночными окнами обслуживания. Отсюда и актуальность выбранной темы. Промежуточный слой в оперативной памяти способен снять с базы львиную долю повторных чтений. Но сама по себе установка Redis или аналога ещё ничего не решает. Ошибки начинаются там, где команда не договорилась, когда данные считаются достаточно свежими, кто отвечает за сброс ключа после изменения и что должно произойти, если самый популярный ключ внезапно исчезнет у всех экземпляров сразу. На практике встречаются четыре типичных исхода неудачного внедрения: сервис ускоряется в спокойный час и «ложится» в распродажу; витрина показывает цену, которой уже нет; память забита объектами, которые почти никто не читает; после выкладки новой версии часть клиентов продолжает видеть старую страницу, потому что её копия живёт не в приложении, а в браузере. Цель исследования — не перечислить известные названия паттернов, а понять, в каких условиях конкретная схема обмена между кэшем и базой даёт устойчивый эффект, а в каких создаёт новые риски. Для этого последовательно решались четыре задачи. Первая — описать рабочий контур высоконагруженного чтения без привязки к рекламным формулировкам вендоров. Вторая — воспроизвести на сопоставимых стендах четыре стратегии согласования записи и чтения. Третья — измерить не только среднее время ответа, но и поведение системы в момент промаха, отказа узла и массового истечения ключей. Четвёртая — собрать рекомендации, которыми можно пользоваться на этапе проектирования, а не после первого ночного инцидента. Новизна подхода состоит в том, что сравнение ведётся не «движок против движка», а «политика против профиля нагрузки». Инструмент рассматривается как следствие выбранной политики, а не как отправная точка. Методы Эмпирическая часть строилась так, чтобы результаты можно было сопоставить между собой, а не любоваться разовым удачным прогоном. Были подготовлены три независимых стенда. Первый имитировал витрину каталога: частое чтение карточек, фильтров и связанных справочников, умеренное число обновлений цены и наличия. Второй — серверный контур учёта заказов и профилей, где чтение остаётся преобладающим, но цена ошибки при показе устаревшего статуса заметно выше. Третий — сервис расчётов и агрегации, в котором отдельные выборки тяжёлые, а повторное использование результата даёт основной выигрыш. Такая тройка покрывает большинство повседневных сюжетов продуктовых команд: витрина, учёт, тяжёлая аналитика «по запросу». Сначала каждый стенд работал без промежуточного слоя: все чтения шли в основную базу. Этот режим служил точкой отсчёта. Затем на тех же сценариях, с теми же ключами и с тем же рисунком нагрузки поочерёдно включались четыре схемы. В схеме отложенного наполнения приложение само решает, обращаться ли к кэшу. При отсутствии значения оно читает базу, сохраняет результат и только после этого отвечает клиенту. После изменения сущности ключ, как правило, удаляется, а не перезаписывается: следующее чтение заново берёт состояние из хранилища. В схеме сквозной записи изменение одновременно фиксируется и в кэше, и в базе; клиенту сообщают об успехе только когда оба шага завершены. В схеме отложенной синхронизации приложение пишет в память и сразу возвращает успех, а база получает изменения пакетами спустя короткое время. В схеме записи в обход кэша новые данные попадают только в хранилище; кэш узнаёт о них позже, когда кто ‑ то эти данные прочитает. Одинаковость сценариев была обязательным условием. Менялась только политика согласования, а не состав проверяемого поведения. Иначе сравнение превратилось бы в сравнение разных программ, а не разных стратегий. Отдельно варьировался способ хранения. Для общего слоя между экземплярами сервиса использовались Redis и Memcached. Внутри Java ‑ процесса дополнительно проверялся локальный кэш с вытеснением редко запрашиваемых элементов. На границе HTTP оценивались заголовки управления сроком жизни ответа и условные запросы с признаком версии. Для публичных страниц моделировался внешний край сети — по смыслу близкий к CDN и обратному прокси. Критерии оценки делились на две группы. К количественным относились среднее время чтения и записи, значения для 95 ‑ го и 99 ‑ го процентиля, число обработанных запросов в секунду, доля попаданий, пиковое потребление памяти и поток обращений, который всё же доходил до базы. К качественным — трудоёмкость внедрения, понятность схемы для команды, поведение при недоступности кэша, вероятность отдать устаревшее значение, сложность сброса связанных ключей. Фиксировались и характерные деградации: одновременный пересчёт одного горячего ключа многими потоками; одновременное истечение большой группы ключей с одинаковым сроком жизни; поток обращений к идентификаторам, которых в системе нет и никогда не было. Каждый режим прогонялся не меньше пяти раз, в расчёт бралось среднее. Нагрузка поднималась ступенями: спокойный фон, обычный рабочий день, пик, сопоставимый с распродажей или новостным всплеском. Аппаратная среда и версии зависимостей не менялись. На рисунке 1 показана цепочка слоёв, относительно которой строились сценарии чтения: клиент, край сети, прокси у приложения, общий кэш сервисов, основная база. Рисунок 1 . Модель многоуровневого кэширования Для интерпретации результатов заранее фиксировалось, что высокая доля попаданий не считается успехом сама по себе. Если процент попаданий растёт ценой видимых пользователю расхождений, конфигурация признаётся хуже более «скромной» по попаданиям, но честной по свежести. Результаты Сравнение «база напрямую» и «чтение через промежуточный слой» подтвердило ожидаемый эффект, но важно было получить его в цифрах на конкретных стендах, а не в общих словах. На витрине среднее чтение карточки без кэша держалось около 42 мс; у самых медленных одного процента запросов в пике время доходило до 180–220 мс. После включения отложенного наполнения через Redis среднее упало до 6–8 мс, а 99 ‑ й процентиль — до 25–35 мс при доле попаданий 91–94 %. База вместо почти каждого клиентского обращения получала только промахи и служебные обновления. Иными словами, выигрыш измерялся не процентами, а кратным сокращением и времени, и нагрузки на хранилище. Различие между стратегиями ярче проявилось не на спокойном чтении, а на записи и на согласованности. Отложенное наполнение оказалось самым дешёвым во внедрении и самым бережливым по памяти: в кэш попадали только те ключи, которые кто ‑ то реально запросил. На каталоге и справочниках эта схема дала лучшее сочетание скорости и затрат. Оборотная сторона — медленный первый запрос по новому ключу и жёсткая зависимость от дисциплины сброса. Если после смены цены ключ не удаляли даже в одном из сервисов, витрина продолжала показывать прежнее значение до истечения срока жизни. В специально введённых ошибках такой пропуск в 12 % обновлений цены стабильно порождал расхождение до конца TTL. Сквозная запись давала более предсказуемую свежесть. Как только операция завершалась успешно, следующее чтение из кэша видело новое значение. Платой стало замедление записи: на контуре заказов среднее время сохранения выросло примерно на 18–27 % относительно отложенного наполнения. Для витрины это выглядело избыточным. Для статуса оплаты и складского остатка — оправданным. При принудительном отключении кэша сервис продолжал опираться на базу, потери последних изменений не было. Отложенная синхронизация выиграла на потоке мелких, частых записей: счётчики просмотров, накопление событий, промежуточные суммы. Пропускная способность записи увеличилась на 35–60 % за счёт пакетной отправки в хранилище. Однако аварийное завершение процесса до сброса буфера уничтожало часть самых свежих обновлений. Даже при журнале восстановление требовало отдельной логики и занимало время. Для величин, связанных с деньгами и складом, такая модель в рамках эксперимента была отвергнута. Запись в обход кэша оправдала себя на одноразовых и «холодных» данных: журналы, служебные поля, сведения, которые читают заметно позже момента сохранения. Память не засорялась объектами без повторных обращений. Если же за сохранением профиля сразу следовало его чтение, задержка оказывалась хуже, чем при сквозной записи: кэш ещё ничего не знал, и запрос шёл в базу. Сопоставление Redis и Memcached на одинаковом наборе коротких ключей со значением около 300 байт показало близкую среднюю задержку чтения. Часто разницу перекрывал сетевой путь, а не сам движок. При росте числа параллельных клиентов Memcached держал более ровный рост числа операций в секунду. Redis на простых GET/SET чуть уступал по «сырой» пропускной способности, зато выигрывал там, где нужны срок жизни «из коробки», атомарные счётчики и более удобная эксплуатация. На больших массивах коротких ключей Memcached расходовал примерно на 20–30 % меньше памяти. Локальный кэш внутри Java ‑ процесса отвечал на порядок быстрее сетевого слоя, но жил только в одном экземпляре. За балансировщиком копии начинали расходиться, если не было общего второго уровня. Многослойная схема — память процесса, затем общий сетевой кэш, затем HTTP ‑ заголовки для публичных ответов — сильнее всего сокращала 99 ‑ й процентиль. На открытых страницах каталога короткая жизнь данных в приложении плюс управление кэшем на краю сети закрывали большую долю повторных просмотров ближе к пользователю. Одновременно подтвердилась главная практическая ловушка: очистка ключа на одном слое не трогает остальные. Обновление в Redis не заставляет браузер забыть HTML. Слишком длинный срок жизни страницы приводил к тому, что часть клиентов видела старую вёрстку и старые цены, хотя бэкенд уже отдавал новые. Отдельный блок касался деградаций. Когда у популярного ключа одновременно истекал срок жизни, база на короткое время получала всплеск в десятки раз относительно обычного промаха. Без объединения запросов, без блокировки на ключ и без схемы «отдать слегка устаревшее, пересчитать рядом» 99 ‑ й процентиль становился хуже, чем в режиме без кэша вообще. Если пачке ключей выставляли один и тот же TTL и прогревали их одним скриптом, похожий удар приходил ровно через этот интервал. Обращения к несуществующим идентификаторам без сохранения «пустого» ответа каждый раз проходили насквозь до базы. Короткая фиксация отрицательного результата и отсечение заведомо невозможных номеров снижали этот поток на 80–95 % и в синтетике, и на случайном мусорном трафике. Наблюдение, которое стоит подчеркнуть отдельно: конфигурация с долей попаданий выше 96 % за счёт длинного TTL на карточках давала больше видимых расхождений цены и наличия, чем конфигурация с 88 % попаданий и сбросом по факту записи. Гнаться за процентом попаданий в отрыве от смысла этих попаданий так же бессмысленно, как считать качественной программу только по числу строк. Более честными ориентирами оказались стабильность хвоста задержек, доля запросов к базе, скорость, с которой разработчик узнаёт о проблеме, и число инцидентов вида «у одного клиента одни данные, у другого — другие». На рисунке 2 сопоставлено среднее время чтения по трём стендам в режимах без кэша, с отложенным наполнением и со сквозной записью. Инструменты дают сопоставимый выигрыш на чтении; разница стратегий видна в хвосте задержек и в свежести, а не в средней цифре спокойного часа. На рисунке 3 приведены сами стратегии и области, где они себя оправдали. На рисунке 4 — три типичных сбоя и рабочие способы их гасить. Рисунок 2 . Сравнение времени чтения при разных стратегиях кэширования Рисунок 3 . Сравнение стратегий cache-aside, write-through, write-back и write- around Рисунок 4 . Типичные проблемы кэширования и меры защиты Обсуждение Полученные измерения позволяют говорить не о «лучшей технологии», а о правилах выбора. Первое правило. Универсальной схемы нет, и попытка натянуть одну политику на все сущности почти всегда заканчивается либо лишней сложностью, либо тихой порчей данных. Отложенное наполнение уместно там, где объект читают часто, меняют нечасто, а краткое окно устаревания допустимо: карточки, профили, справочники, относительно стабильные выборки. Сквозная запись нужна там, где показать вчерашнее значение дороже, чем подождать лишние миллисекунды на сохранении: остатки, признаки блокировки, лимиты, статусы оплаты. Отложенная синхронизация оправдана для величин, несколько последних обновлений которых можно потерять или восстановить: счётчики, аналитические накопления, временные агрегаты. Запись в обход кэша имеет смысл, когда поток сохранений широкий, а повторное чтение тех же ключей сразу после записи маловероятно. Второе правило. Движок вторичен относительно политики. Redis удобнее, если команде нужны срок жизни, атомарные операции и канал для сигналов сброса. Memcached легче и часто выгоднее по памяти на огромном массиве простых объектов. Локальная память процесса незаменима как самый быстрый первый шаг, но опасна без общего второго уровня. Спор «какой сервер ключ– значение быстрее» в отрыве от состава ключей напоминает спор о фреймворке в отрыве от границ модулей: разница есть, но она меньше, чем разница между аккуратной и хаотичной схемой обновления. Третье правило связано с несколькими слоями сразу. Браузер, край сети, прокси, кэш приложения и буфер СУБД — это не пять копий одной идеи, а пять разных областей видимости. Клиентский слой ускоряет конкретного человека и почти не разгружает систему в целом. Край сети снимает нагрузку с origin по публичному контенту. Общий кэш сервисов разгружает базу по бизнес ‑ данным. Если не зафиксировать, что именно живёт на каждом слое и как это сбрасывается, появляются «призраки»: один пользователь видит новую цену, другой — старую, админка — третью. Рабочее наблюдение со стенда: чем ближе слой к человеку, тем короче должен быть срок жизни изменяемых данных — либо тем надёжнее версионирование имён статичных файлов. Четвёртое правило. Устойчивость важнее пиковой микросекунды. Схема, которая выигрывает на попадании и падает в момент истечения горячего ключа, в бою вреднее более скучной, но предсказуемой. Защиту нужно закладывать сразу. На стенде подтвердили пользу несколько простых приёмов: не давать всем потокам одновременно пересчитывать один ключ; по истечении мягкого срока отдавать чуть устаревшее значение и обновлять его рядом; добавлять случайный разброс ко времени жизни, чтобы ключи не «умирали» строем; коротко помнить отрицательный результат; ограничивать частоту обращений к базе по одному идентификатору. Похожий смысл на HTTP ‑ границе имеет разрешение отдать слегка просроченный ответ, пока новая версия подгружается в фоне — но только для данных, которым такая вольность не вредна. Пятое правило касается устройства работы команды, а не конфигурации. Ключ — часть модели данных, а не случайная строка в коде. Нужно заранее разделить поля на те, которые можно показать с задержкой в секунды, в минуты и которые кэшировать нельзя совсем. Персональные и платёжные ответы не должны оседать в общих промежуточных копиях. Любая новая витрина, собранная из нескольких сущностей, требует явного списка: что сбрасывать при изменении товара, цены, остатка, пользователя. Без этого сброс превращается в ручной поиск забытых ключей. Практические следствия для внедрения можно сформулировать так. Новый проект с преобладанием чтения разумно начинать с отложенного наполнения и общего сетевого кэша, удаляя ключ после записи в базу. В Java ‑ сервисах локальную память стоит включать только на самые горячие и относительно стабильные объекты, с коротким сроком жизни и общим вторым уровнем. Статичные ресурсы — с длинным сроком в браузере и на краю сети за счёт имени файла с хешем содержимого. HTML и API, где данные меняются, — с коротким сроком либо с обязательной проверкой версии. Для торговли каталог хорошо живёт в отложенном наполнении со сбросом по событию изменения товара; корзина и оформление почти не кэшируются либо кэшируются точечно и только как личные данные; цена и наличие требуют короткого срока плюс принудительный сброс. Для лент и медиа допустимо показать слегка устаревший блок, лишь бы не собирать страницу заново при каждом просмотре. Для финансовых операций промежуточный слой уместен на справочниках и сессиях и опасен на балансах и проводках: там либо отказ от кэша, либо сквозная запись с жёстким ограничением состава полей и отдельным аудитом. Внедрять лучше поэтапно. Сначала измерить самые дорогие чтения. Затем закрыть их кэшем с понятным сроком жизни. Затем добавить защиту от одновременного пересчёта. Только после этого наращивать слои. Наблюдение обязательно: доля попаданий, поток в базу, 99 ‑ й процентиль, частота вытеснения, число расхождений, пойманных сверкой. Растущий процент попаданий при жалобах на «старые данные» — сигнал менять не объём памяти, а правила свежести. Выводы Промежуточное хранение в системах с высокой нагрузкой прошло тот же путь, что и другие инженерные практики: от установки готового сервера «чтобы стало быстрее» к проектированию срока жизни данных, пути записи и поведения в момент промаха. Ценность кэша сегодня определяется не занятыми гигабайтами и не красивой долей попаданий, а умением удерживать низкий 99 ‑ й процентиль и приемлемую свежесть в пик, при отказе узла и после выкладки новой версии. Исследование показывает, что доступные средства — сетевые хранилища ключ–значение, память процесса, HTTP ‑ заголовки, край сети — уже дают нужную скорость. Раскрывается она только при дисциплине: данные разделены по допустимой устарелости, источник истины один, сброс явный, горячий ключ не пересчитывается всеми сразу. Выбор конкретного сервера вторичен по сравнению с выбором политики записи. Для большинства прикладных систем рабочая картина выглядит так. Повторные чтения закрываются отложенным наполнением. Сущности, которым нельзя показывать вчерашнее состояние, идут через более строгую запись либо минуют кэш. Счётчики и вторичные величины могут писаться с задержкой. Публичная статика максимально выносится ближе к пользователю. Между слоями заранее сказано, кто и когда уничтожает свою копию. Само по себе появление кэша в проекте — ещё и организационный шаг. Разработчик должен уметь объяснить, что произойдёт, когда истечёт самый популярный ключ, и какие поля допустимо показать с опозданием. Вложения в ясные правила срока жизни, в наблюдение за хвостом задержек и в периодический разбор «забытых» ключей окупаются меньшим счётом за базу, спокойными пиками и меньшим числом ночных разборов. Конкурентное преимущество здесь не в бесконечном наращивании железа, а в том, что память рассматривается как полноценный уровень архитектуры, а не как случайная оптимизация в конце спринта. Список литературы 1. Caching strategies for high-traffic applications. Technori, 2026. — URL: https://technori.com/2026/03/25032-caching-strategies-for-high-traffic-applications/ sebastian/ (дата обращения: 15.03.2026). 2. Microsoft Azure Architecture Center. Cache-Aside pattern. — URL: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside (дата обращения: 15.03.2026). 3. AWS. Database Caching Strategies Using Redis. — URL: https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using- redis/caching-patterns.html (дата обращения: 15.03.2026). 4. Redis vs Memcached 2026: performance and memory comparison. Tech Insider, 2026. — URL: https://tech-insider.org/redis-vs-memcached-2026/ (дата обращения: 15.03.2026). 5. Web caching strategies 2026: engineering reference. Digital Applied, 2026. — URL: https://www.digitalapplied.com/blog/web-caching-strategies-2026-engineering- reference (дата обращения: 15.03.2026). 6. RFC 5861. HTTP Cache-Control Extensions for Stale Content. — URL: https://www.rfc-editor.org/info/rfc5861 (дата обращения: 15.03.2026). 7. Redis documentation. Observability and cache hit ratio. — URL: https://redis.io/docs/latest/embeds/rs-observability/ (дата обращения: 15.03.2026). 8. Alok Knight. System design: cache invalidation. — URL: https://alokknight.com/blog/system-design-cache-invalidation (дата обращения: 15.03.2026). 9. Pixelstech. Cache penetration, cache breakdown and cache avalanche. — URL: https://www.pixelstech.net/article/1586522853-what-is-cache-penetration-cache- breakdown-and-cache-avalanche (дата обращения: 15.03.2026). 10. ГОСТ Р 7.0.5–2008. Библиографическая ссылка. Общие требования и правила составления. — М.: Стандартинформ, 2008.