Руководство по кэшированию

Управляемый Redis в Azure

Кэширование — это распространенный способ, который предназначен для повышения производительности и масштабируемости системы. Он кэширует данные, временно копируя часто используемые данные в быстрое временное хранилище, расположенное рядом с приложением. Если это быстрое хранилище данных находится ближе к приложению, чем исходный оригинал, кэширование может значительно улучшить время отклика для клиентских приложений путем более быстрой обработки данных.

Кэширование наиболее эффективно, если экземпляр клиента многократно считывает одни и те же данные, особенно если все следующие условия применяются к исходному хранилищу данных:

  • Он остается относительно статическим.
  • Оно является медленным по сравнению со скоростью кэша.
  • Оно подвержено высокому уровню конкуренции.
  • Это достаточно далеко от клиентов, что задержка сети является значительной.

Кэширование в распределенных приложениях

Распределенные приложения обычно реализуют один или оба из следующих стратегий при кэшировании данных:

  • Они используют частный кэш, где данные хранятся локально на компьютере, на котором выполняется экземпляр приложения или службы.
  • Они используют общий кэш, выступая в качестве общего источника, к которому могут получить доступ несколько процессов и компьютеров.

В обоих случаях кэширование может выполняться на стороне клиента и на стороне сервера. Кэширование на стороне клиента выполняется процессом, предоставляющим пользовательский интерфейс для системы, например веб-браузер или классическое приложение. Кэширование на стороне сервера выполняется процессом, предоставляющим бизнес-службы, работающие удаленно.

Частное кэширование

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

Если вам нужно кэшировать больше информации, чем физически возможно в памяти, можно записать кэшированные данные в локальную файловую систему. Этот процесс медленнее, чем данные, хранящиеся в памяти, но все равно должны быть более быстрыми и надежными, чем получение данных в сети.

Если у вас несколько экземпляров приложения, использующего эту модель одновременно, каждый экземпляр приложения имеет собственный независимый кэш, содержащий собственную копию данных.

Подумайте о кэше как моментальном снимке исходных данных в прошлом. Если эти данные не статически, скорее всего, разные экземпляры приложений содержат разные версии данных в своих кэшах. Таким образом, один и тот же запрос, выполняемый этими экземплярами, может возвращать различные результаты, как показано на рис. 1.

Результаты использования кэша в памяти в разных экземплярах приложения

Рис. 1. Использование кэша в памяти в разных экземплярах приложения.

Общее кэширование

Если вы используете общий кэш, это может помочь снизить риск расхождения данных между отдельными кэшами, что часто бывает при кэшировании в памяти. Совместное кэширование гарантирует, что разные экземпляры приложений видят одинаковое представление кэшированных данных. Он находит кэш в отдельном расположении, которое обычно размещается в составе отдельной службы, как показано на рис. 2.

Результаты использования общего кэша

Рис. 2. Использование общего кэша.

Важное преимущество общего подхода к кэшированию — это масштабируемость, которая она предоставляет. Многие службы общего кэша реализуются с помощью кластера серверов и используют программное обеспечение для прозрачного распределения данных в кластере. Экземпляр приложения отправляет запрос в службу кэша. Базовая инфраструктура определяет расположение кэшированных данных в кластере. Вы можете легко масштабировать кэш, добавив дополнительные серверы.

Существует два основных недостатка общего подхода кэширования:

  • Кэш медленнее для доступа, так как он больше не хранится локально для каждого экземпляра приложения.
  • Требование реализации отдельной службы кэша может добавить сложность в решение.

Рекомендации по использованию кэширования

В следующих разделах подробно описаны рекомендации по проектированию и использованию кэша.

Решите, когда кэшировать данные

Кэширование может значительно повысить производительность, масштабируемость и доступность. Чем больше данных у вас есть, тем больше число пользователей, которым требуется доступ к этим данным, тем больше преимущества кэширования становятся. Кэширование уменьшает задержку и состязание, связанные с обработкой больших объемов одновременных запросов в исходном хранилище данных.

Например, база данных может поддерживать ограниченное количество одновременных подключений. Получение данных из общего кэша, а не базовой базы данных, позволяет клиентскому приложению получить доступ к этим данным, даже если количество доступных подключений в настоящее время исчерпано. Кроме того, если база данных становится недоступной, клиентские приложения могут продолжать использовать данные, которые хранятся в кэше.

Рассмотрите возможность кэширования данных, которые часто читаются, но редко изменяются (например, данные, имеющие более высокую долю операций чтения, чем операции записи). Однако мы не рекомендуем использовать кэш в качестве авторитетного хранилища критически важных сведений. Вместо этого убедитесь, что все изменения, которые приложение не может позволить себе потерять, всегда сохраняются в постоянном хранилище данных. Если кэш недоступен, приложение по-прежнему может работать с помощью хранилища данных, и вы не потеряете важные сведения.

Определение эффективного кэширования данных

Ключ к использованию кэша заключается в определении наиболее подходящих данных для кэширования и кэшировании в соответствующее время. Данные можно добавить в кэш по мере необходимости при их первом получении приложением. Приложение должно получить данные только один раз из хранилища данных, а затем последующий доступ может быть удовлетворен с помощью кэша.

Кроме того, кэш может быть частично или полностью заполнен данными заранее, как правило, при запуске приложения (подход, известный как заполнение). Однако, возможно, не рекомендуется реализовать инициализацию для крупного кэша, так как этот подход может привести к внезапно высокой нагрузке на исходное хранилище данных вскоре после запуска приложения.

Часто анализ шаблонов использования помогает решить, следует ли полностью или частично предварительно заполнить кэш, а также выбрать данные для кэширования. Например, можно заполнить кэш статическими данными профиля пользователя для клиентов, которые регулярно используют приложение (возможно, каждый день), но не для клиентов, которые используют приложение только один раз в неделю.

Кэширование обычно хорошо работает с данными, которые неизменяемы или изменяются редко. Примеры включают справочные сведения, такие как сведения о продукте и ценах в приложении электронной коммерции, или общие статические ресурсы, которые являются дорогостоящими для создания. Некоторые или все эти данные можно загрузить в кэш при запуске приложения, чтобы свести к минимуму спрос на ресурсы и повысить производительность. Также может понадобиться фоновый процесс, который периодически будет обновлять справочные данные в кэше, чтобы убедиться, что они актуальны. Или фоновый процесс может обновить кэш при изменении ссылочных данных.

Кэширование менее полезно для динамических данных, хотя в этом вопросе существуют некоторые исключения. Дополнительные сведения см. в разделе "Кэш с высокой динамической динамикой данных " далее в этой статье. Когда исходные данные регулярно изменяются, кэшированная информация быстро устаревает, и накладные затраты на синхронизацию кэша с исходным хранилищем данных снижают эффективность кэширования.

Кэш не должен включать полные данные для сущности. Например, если элемент данных представляет многозначный объект, например банковский клиент с именем, адресом и балансом счета, некоторые из этих элементов могут оставаться статическими, такими как имя и адрес. Другие элементы, такие как баланс учетной записи, могут быть более динамичными. В таких ситуациях может быть полезно кэшировать статические части данных и извлекать (или вычислять) только оставшиеся сведения, если это необходимо.

Рекомендуется выполнить тестирование производительности и анализ использования, чтобы определить, подходит ли предварительное заполнение или загрузка кэша по запросу или сочетание обоих. Решение должно основываться на модели волатильности и использования данных. Использование кэша и анализ производительности важны в приложениях, которые сталкиваются с тяжелыми нагрузками и должны быть высокомасштабируемыми. Например, в высокомасштабируемых сценариях можно заполнить кэш, чтобы уменьшить нагрузку на хранилище данных в пиковое время.

Кэширование также можно использовать для предотвращения повторяющихся вычислений во время работы приложения. Если операция преобразует данные или выполняет сложные вычисления, она может сохранить результаты операции в кэше. Если после этого требуется то же вычисление, приложение может получить результаты из кэша.

Приложение может изменять данные, которые хранятся в кэше. Однако мы рекомендуем думать о кэше как временном хранилище данных, которое может исчезнуть в любое время. Не сохраняйте ценные данные только в кэше; Убедитесь, что данные также хранятся в исходном хранилище данных. Это означает, что если кэш становится недоступным, можно свести к минимуму вероятность потери данных.

Кэшировать высокодинамические данные

При быстром изменении информации в постоянном хранилище данных она может наложить на систему издержки. Например, рассмотрим устройство, которое постоянно сообщает о состоянии или другом измерении. Если приложение не кэширует эти данные на основе того, что кэшированные сведения обычно устарели, то при хранении и получении этих сведений из хранилища данных может быть верно то же самое. В течение времени, необходимого для сохранения и получения этих данных, это может измениться.

В такой ситуации рассмотрите преимущества хранения динамической информации непосредственно в кэше, а не в постоянном хранилище данных. ** Если данные некритичны и не требуют аудита, то не имеет значения, если случайные изменения будут утеряны.

Управление истечением срока действия данных в кэше

В большинстве случаев данные, содержащиеся в кэше, являются копией данных, которые хранятся в исходном хранилище данных. Данные в исходном хранилище данных могут измениться после его кэширования, что приведет к тому, что кэшированные данные становятся устаревшими. Многие системы кэширования позволяют настроить срок действия кэша и сократить период, в течение которого данные могут быть устаревшими.

Истекший срок действия кэшированных данных удаляется из кэша, и приложение должно получить данные из исходного хранилища данных (он может поместить только что полученные данные в кэш). Политику истечения срока действия по умолчанию можно задать при настройке кэша. Во многих службах кэша можно также указать срок действия отдельных объектов при их хранении программным способом в кэше. Некоторые кэши позволяют указать срок действия как абсолютное значение или как скользящее значение, которое приводит к удалению элемента из кэша, если доступ к нему не выполняется в течение указанного времени. Этот параметр переопределяет любую политику истечения срока действия кэша, но только для указанных объектов.

Note

Рассмотрим период истечения срока действия кэша и объектов, которые он содержит тщательно. Если сделать его слишком коротким, срок действия объектов истекает слишком быстро, и вы сокращаете преимущества использования кэша. Если вы делаете период слишком длинным, вы рискуете, что данные становятся устаревшими.

Также возможно, что кэш может заполниться, если данные могут оставаться резидентными в течение длительного времени. В этом случае любые запросы на добавление новых элементов в кэш могут привести к принудительному удалению некоторых элементов в процессе, известном как вытеснение. Службы кэша обычно вытесняют данные на основе принципа 'наименее недавно использованного' (LRU), но обычно можно изменить эту политику и предотвратить удаление элементов. Однако если вы используете этот подход, вы рискуете превысить объем памяти, доступной в кэше. Приложение, которое пытается добавить элемент в кэш, не сможет это сделать из-за возникновения исключения.

Некоторые реализации кэширования могут предоставлять другие алгоритмы вытеснения. Существует несколько типов политик вытеснения. К ним относятся:

  • Политика наименее недавно использованных данных (в ожидании того, что эти данные не потребуются снова).
  • Политика очереди (самые старые данные вытесняются первыми).
  • Явная политика удаления на основе триггерного события (например, измененных данных).

Недопустимые данные в клиентском кэше

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

Это означает, что клиент, использующий плохо настроенный кэш, может продолжать использовать устаревшие сведения. Например, если политики истечения срока действия кэша не реализованы должным образом, клиент может использовать устаревшие сведения, кэшированные локально при изменении информации в исходном источнике данных.

Если вы создаете веб-приложение, которое передает данные через HTTP-соединение, вы можете неявно заставить веб-клиент (например, браузер или веб-прокси) получить самую свежую информацию. Это можно сделать, если ресурс обновляется изменением URI этого ресурса. Веб-клиенты обычно используют URI ресурса в качестве ключа в клиентском кэше, поэтому если URI изменяется, веб-клиент игнорирует все ранее кэшированные версии ресурса и получает новую версию.

Управление параллелизмом в кэше

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

В зависимости от характера данных и вероятности конфликтов можно применить один из двух подходов к параллелизму:

  • Optimistic. Перед обновлением данных приложение проверяет, изменились ли данные в кэше после получения. Если данные по-прежнему одинаковы, можно внести изменения. В противном случае приложению необходимо решить, следует ли обновить его. (Бизнес-логика, которая управляет этим решением, зависит от приложения.) Этот подход подходит для ситуаций, когда обновления нечасто или где столкновения вряд ли возникают.
  • Pessimistic. При получении данных приложение блокирует его в кэше, чтобы предотвратить изменение другого экземпляра. Этот процесс гарантирует, что столкновения не могут возникать, но при этом он может блокировать другие экземпляры, которые должны обрабатывать те же данные. Пессимистичная параллельность может сказываться на масштабируемости решения и рекомендуется использовать только для краткосрочных операций. Этот подход может быть подходящим для ситуаций, когда столкновения более вероятны, особенно если приложение обновляет несколько элементов в кэше и должно обеспечить согласованность применения этих изменений.

Реализация высокой доступности и масштабируемости и повышение производительности

Избегайте использования кэша в качестве основного репозитория данных; это роль исходного хранилища данных, из которого заполняется кэш. Исходное хранилище данных отвечает за сохранение данных.

Будьте осторожны, чтобы не вводить критические зависимости от доступности общей службы кэша в ваших решениях. Приложение должно продолжать работу, если служба, предоставляющая общий кэш, недоступна. Приложение не должно становиться неотзывчивым или завершаться сбоем во время ожидания возобновления работы службы кэша.

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

Однако масштабируемость системы может повлиять, если приложение вернется к исходному хранилищу данных, когда кэш временно недоступен. В то время как хранилище данных восстанавливается, исходное хранилище может быть перегружено запросами на данные, что ведет к истечению времени ожидания и неудачным подключениям.

Рассмотрите возможность реализации локального частного кэша в каждом экземпляре приложения вместе с общим кэшем, к которому обращаются все экземпляры приложения. Когда приложение получает элемент, он может сначала проверить его в локальном кэше, а затем в общем кэше и, наконец, в исходном хранилище данных. Локальный кэш можно заполнить с помощью данных в общем кэше или в базе данных, если общий кэш недоступен.

Этот подход требует тщательной настройки, чтобы предотвратить устаревание локального кэша по сравнению с общим кэшем. Однако локальный кэш выступает в качестве буфера, если общий кэш недоступен. На рисунке 3 показана эта структура.

Использование локального частного кэша с общим кэшем

Рис. 3. Использование локального частного кэша с общим кэшем.

Для поддержки больших кэшей, которые содержат относительно долгоживущие данные, некоторые службы кэша предоставляют высокую доступность, которая реализует автоматическое переключение при сбое в случае, если кэш становится недоступен. Этот подход обычно включает копирование кэшированных данных, хранящихся на сервере первичного кэша, на сервер вторичного кэша и переключение на вторичный сервер, если первичный сервер выходит из строя или теряется соединение.

Чтобы уменьшить задержку, связанную с записью в несколько мест, репликация на дополнительный сервер может происходить асинхронно при записи данных в кэш на основном сервере. Такой подход приводит к тому, что некоторые кэшированные сведения могут быть потеряны при сбое, но доля этих данных должна быть небольшой, по сравнению с общим размером кэша.

Если общий кэш велик, может оказаться полезным секционировать кэшированные данные по узлам, чтобы снизить вероятность возникновения спорных ситуаций и повысить масштабируемость. Многие общие кэши поддерживают возможность динамического добавления (и удаления) узлов и перебалансирования данных между секциями. Этот подход может включать кластеризацию, в котором коллекция узлов представлена клиентским приложениям в виде одного кэша. Внутри системы, данные распределяются между узлами по предопределенной стратегии распределения, равномерно распределяя нагрузку. Дополнительные сведения см. в шаблоне сегментирования.

Кластеризация также может увеличить доступность кэша. Если узел выйдет из строя, оставшаяся часть кэша по-прежнему доступна. Кластеризация часто используется с репликацией и отказоустойчивостью. Каждый узел можно реплицировать, и реплика может быть быстро введена в эксплуатацию, если узел выходит из строя.

Многие операции чтения и записи, скорее всего, связаны с одними значениями данных или объектами. Однако иногда может потребоваться хранить или извлекать большие объемы данных. Например, создание кэша может включать запись сотен или тысяч элементов в кэш. Кроме того, приложению может потребоваться получить большое количество связанных элементов из кэша в рамках того же запроса.

Многие крупномасштабные кэши предоставляют пакетные операции для этих целей. Это позволяет клиентскому приложению упаковать большой объем элементов в один запрос и сократить затраты, связанные с выполнением большого количества небольших запросов.

Кэширование и итоговая согласованность

Для работы паттерна обхода кэша, экземпляр приложения, который заполняет кэш, должен иметь доступ к последней и согласованной версии данных. В системе, реализующей итоговую согласованность (например, реплицированное хранилище данных), это может быть не так.

Один экземпляр приложения может изменить объект данных и аннулировать кэшированную версию этого объекта. Другой экземпляр приложения может попытаться прочитать этот элемент из кэша, что приводит к пропуску кэша, поэтому он считывает данные из хранилища данных и добавляет его в кэш. Однако если хранилище данных не полностью синхронизировано с другими репликами, экземпляр приложения может считывать и заполнять кэш старым значением.

Распределенный кэш представляет еще один уровень этой проблемы. Теорема CAP утверждает, что распределенная система может обеспечить только два из трех гарантий: согласованность, доступность и устойчивость к разделениям. Так как сетевые секции неизбежны в облачных средах, необходимо выбрать между согласованностью и доступностью. Большинство распределенных кэшей, включая Redis, отдают приоритет доступности и устойчивости разделов в ущерб строгой согласованности. Это означает, что операции чтения из реплики кэша могут возвращать устаревшие данные во время сетевой секции или сразу после записи на другой узел. При разработке стратегии кэширования определите, сколько устаревания может допустить ваше приложение, и задайте время жизни (TTL) соответствующим образом. Для данных, которые должны быть текущими, используйте более короткие списки TTL или обходить кэш полностью и считывать из исходного хранилища данных.

Дополнительные сведения об обработке согласованности данных в распределенных системах см. в рекомендациях по обработке микрослужб.

Защита кэшированных данных

Независимо от используемой службы кэша рассмотрите возможность защиты данных, которые хранятся в кэше от несанкционированного доступа. Существует две основные проблемы:

  • Конфиденциальность данных в кэше.
  • Конфиденциальность данных по мере перемещения между кэшем и приложением, которое его использует.

Чтобы защитить данные в кэше, служба кэша может реализовать механизм проверки подлинности, который требует, чтобы приложения указали следующие сведения:

  • Какие идентификации могут получить доступ к данным в кэше.
  • Какие операции (чтение и запись) могут выполнять эти удостоверения.

Чтобы уменьшить затраты, связанные с чтением и записью данных, после предоставления идентификатору прав на запись или чтение в кэше, этот идентификатор может использовать любые данные в кэше.

Если необходимо ограничить доступ к подмножествам кэшированных данных, можно выполнить один из следующих подходов:

  • Разделите кэш на секции (с помощью разных серверов кэша) и предоставьте доступ только удостоверениям для секций, которые они должны использовать.
  • Зашифруйте данные в каждом подмножестве с помощью разных ключей и предоставьте ключи шифрования только удостоверениям, которые должны иметь доступ к каждому подмножеству. Клиентское приложение по-прежнему может получить все данные в кэше, но только сможет расшифровать данные, для которых он имеет ключи.

Кроме того, необходимо защитить данные в процессе их поступления в кэш и выхода из него. Вы для этого полагаетесь на функции безопасности, предоставляемые сетевой инфраструктурой, которую используют клиентские приложения для подключения к кэшу. Если кэш реализуется с помощью локального сервера в той же организации, где размещаются клиентские приложения, изоляция самой сети может не потребовать дополнительных действий. Если кэш находится удаленно и требует подключения TCP или HTTP через общедоступную сеть (например, Интернет), рассмотрите возможность реализации SSL.

Реализация кэширования с помощью Управляемого Redis в Azure

В остальных разделах этой статьи описывается реализация шаблонов кэширования, описанных выше с помощью Управляемого Redis в Azure. Управляемый сервис Redis в Azure — это управляемая служба Redis, которую можно использовать в качестве общего кэша для экземпляров приложений. Он поддерживает кэширование значений ключа, структуры данных, такие как наборы, отсортированные наборы и списки, а также необязательное сохранение устойчивости во время перезапусков.

Сведения о доступных уровнях, планировании емкости, сети и компонентах см. в документации по Управляемому Redis в Azure.

Подключение и настройка клиентских приложений

Redis поддерживает клиентские приложения на многих языках программирования. Для приложений .NET доступны несколько клиентских библиотек, которые подходят для разных рабочих нагрузок Redis. Выбор соответствующей библиотеки зависит от того, используется ли Redis строго в качестве кэша или в качестве платформы данных с несколькими моделями.

Чтобы подключиться к серверу Redis, используйте статический Connect метод ConnectionMultiplexer класса. Соединение, которое создает этот метод, создается для использования на протяжении всего времени существования клиентского приложения. Одно и то же подключение может использоваться несколькими параллельными потоками. Не подключайтесь и не отключайтесь при каждом выполнении операции Redis, так как это может снизить производительность.

Примеры подключения для конкретного языка см. в разделе "Подключение к Управляемому Redis Azure".

Выбор клиентской библиотеки .NET

При использовании управляемого кеша Redis от Azure для кэширования рекомендуется использовать следующие библиотеки .NET:

  • StackExchange.Redis: низкоуровневый клиент Redis с высокой производительностью. Используйте его, если вам нужен прямой доступ к командам Redis, атомарным операциям, транзакциям, конвейеру или скрипту Lua.
  • Microsoft.Extensions.Caching.StackExchangeRedis: предоставляет интеграцию IDistributedCache для ASP.NET Core. Используйте его для простого кэширования пар «ключ-значение», где значения хранятся как неявные байтовые массивы. Эта абстракция не предоставляет расширенные структуры данных Redis.

Эти библиотеки предоставляют примитивы, необходимые для создания общих шаблонов кэширования, но приложение должно реализовать саму логику кэширования.

Реализация шаблонов кэширования

Самый простой способ использования Redis для кэширования заключается в хранении значений под ключами с помощью модели "ключ-значение". Значения могут быть строками или двоичными данными произвольной длины, что позволяет Redis хорошо подходит для кэширования сериализованных объектов, данных конфигурации, состояния сеанса или предварительно вычисляемых результатов.

Тщательно спроектируйте пространство ключей и используйте значимые (но не излишне длинные) ключи. Например, используйте структурированные ключи( например customer:100 , вместо просто 100) для представления ключа для клиента с идентификатором 100. Эта схема позволяет различать значения, которые хранят разные типы данных. Например, можно также использовать ключ orders:100 для заказа с идентификатором 100.

Хотя строки являются наиболее распространенным подходом к кэшированию, Redis поддерживает широкий набор собственных типов данных, таких как хэши, списки, наборы, отсортированные наборы и потоки, что обеспечивает более гибкие шаблоны кэширования. Дополнительные сведения о типах данных Redis см. в документации Redis по типам данных.

Реализация паттерна кэширования типа cache-aside

Как описано в разделе "Определение эффективного кэширования данных", распространенный подход заключается в загрузке данных в кэш по запросу. В следующем примере сначала проверяется кэш, при отсутствии данных они извлекаются из источника и результат сохраняется для последующих запросов.

var config = new ConfigurationOptions();
// ... configure endpoint, credentials, SSL, etc.
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase db = redisHostConnection.GetDatabase();

async Task<string> RetrieveItemAsync(string itemKey)
{
    // Attempt to retrieve the item from the Redis cache
    string itemValue = await db.StringGetAsync(itemKey);

    // If the value returned is null, the item was not found in the cache
    // So retrieve the item from the data source and add it to the cache
    if (itemValue is null)
    {
        itemValue = await GetItemFromDataSourceAsync(itemKey);
        await db.StringSetAsync(itemKey, itemValue);
    }

    return itemValue;
}

Выполнение атомарных и пакетных операций

Если несколько клиентов или экземпляров приложений совместно используют кэш, необходимо предотвратить одновременное обновление данных, чтобы избежать их повреждения. Общие стратегии параллелизма описаны в разделе "Управление параллелизмом в кэше " ранее в этой статье. Redis предоставляет несколько механизмов реализации этих стратегий.

Атомарные операции с одним ключом. Такие команды, как INCR, INCRBY, DECR, DECRBY и GETSET обновляют значение за один шаг, устраняя условия гонки, возникающие, когда GET и SET выполняются отдельно. Примеры:

  • INCR, INCRBY, DECRDECRBY атомарно увеличивает или уменьшает числовое значение. В StackExchange.Redis используйте IDatabase.StringIncrementAsync и IDatabase.StringDecrementAsync. Это полезно для счетчиков, ограничений скорости и отслеживания квот, в которых несколько клиентов одновременно обновляют один и тот же ключ.

  • GETSET, который атомарно задает ключ новому значению и возвращает предыдущее значение. В StackExchange.Redis используйте IDatabase.StringGetSetAsync:

    string oldValue = await cache.StringGetSetAsync("data:counter", 0);
    

Операции с несколькими ключами. MGET и MSET читают или записывают несколько строковых значений за один запрос, снижая нагрузку на сеть, когда необходимо работать с несколькими ключами одновременно. Методы IDatabase.StringGetAsync и IDatabase.StringSetAsync перегружены для поддержки этой функциональности:

// Create a list of key-value pairs
var keysAndValues =
    new KeyValuePair<RedisKey, RedisValue>[]
    {
        new("data:key1", "value1"),
        new("data:key99", "value2"),
        new("data:key322", "value3")
    };

// Store the list of key-value pairs in the cache
await cache.StringSetAsync(keysAndValues);
...
// Find all values that match a list of keys
RedisKey[] keys = ["data:key1", "data:key99", "data:key322"];
// values should contain { "value1", "value2", "value3" }
RedisValue[] values = await cache.StringGetAsync(keys);

Транзакции (оптимистическая конкуренция). Вы можете использовать WATCH команду для отслеживания одного или нескольких ключей перед запуском транзакции.MULTI/EXEC Если какое-либо отслеживаемое изменение ключа произойдет до начала транзакции, Redis отменяет транзакцию, и клиент может попробовать снова. Библиотека StackExchange обеспечивает поддержку транзакций через интерфейс ITransaction.

Вы создаете объект ITransaction с помощью метода IDatabase.CreateTransaction. Команды для транзакции вызываются с помощью методов, предоставляемых ITransaction объектом.

Интерфейс ITransaction предоставляет доступ к набору методов, которые похожи на те, к которым обращается IDatabase интерфейс, за исключением того, что все методы являются асинхронными. Это означает, что они выполняются только при вызове ITransaction.Execute метода. Значение, возвращаемое методом ITransaction.Execute , указывает, успешно ли была создана транзакция (true) или если она завершилась ошибкой (false).

В следующем фрагменте кода показан пример, который увеличивает и уменьшает два счетчика в рамках одной транзакции:

ITransaction transaction = cache.CreateTransaction();

var tx1 = transaction.StringIncrementAsync("data:counter1");
var tx2 = transaction.StringDecrementAsync("data:counter2");

bool result = await transaction.ExecuteAsync();

Console.WriteLine($"Transaction {(result ? "succeeded" : "failed")}");

if (result)
{
    long increment = await tx1;
    long decrement = await tx2;

    Console.WriteLine($"Result of increment: {increment}");
    Console.WriteLine($"Result of decrement: {decrement}");
}

Транзакции Redis отличаются от транзакций в реляционных базах данных. Метод Execute ставит в очередь все команды, составляющие транзакцию на выполнение, и если какая-либо команда не является допустимой, транзакция останавливается. Если все команды были успешно поставлены в очередь, каждая команда выполняется асинхронно. Если любая команда завершается ошибкой, остальные по-прежнему продолжают обработку. Если необходимо убедиться, что команда выполнена успешно, получите результаты с помощью свойства Result соответствующей задачи, как показано в предыдущем примере.

Скрипты Lua. Для многошаговых обновлений, которые должны быть атомарными по нескольким ключам, вы можете выполнить скрипт Lua на сервере. Redis выполняет весь скрипт как одну операцию без переключения других команд.

Note

В кластеризованных развертываниях все ключи, участвующие в транзакции или скрипте Lua, должны находиться в одном хэш-слоте. Используйте хэш-теги (например, customer:{123}:name и customer:{123}:email) для совместного размещения связанных ключей.

Выполнение операций кэширования с режимом «запустил и забыл»

Если обновление кэша не влияет на правильность приложения, например увеличение счетчика представления или обновление некритической статистики, можно пропустить ожидание ответа сервера. Redis поддерживает операции fire-and-forget с помощью флагов команд, которые снижают задержку кругового пути для клиента:

await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);

Укажите автоматически истекающий срок действия ключей

Стратегии истечения срока действия, описанные в разделе "Управление истечением срока действия данных в кэше ", реализуются в Redis с помощью TTLs для каждого ключа. При хранении элемента в кэше Redis можно указать время ожидания, после которого элемент автоматически удаляется. Вы также можете запросить, сколько времени осталось до истечения срока действия ключа, с помощью команды TTL. Эта команда доступна для приложений StackExchange с помощью IDatabase.KeyTimeToLive метода.

В следующем фрагменте кода показано, как задать время окончания срока действия ключа в течение 20 секунд и запросить оставшееся время существования ключа:

// Add a key with an expiration time of 20 seconds
await cache.StringSetAsync("data:key1", 99, TimeSpan.FromSeconds(20));
...
// Query how much time a key has left to live
// If the key has already expired, the KeyTimeToLive function returns null
TimeSpan? expiry = cache.KeyTimeToLive("data:key1");

Вы также можете задать срок действия определенной даты и времени с помощью EXPIREAT команды, которая доступна в библиотеке StackExchange в качестве KeyExpireAsync метода с параметром DateTime :

await cache.StringSetAsync("data:key1", 99);
await cache.KeyExpireAsync("data:key1",
    new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc));

Tip

Вы можете вручную удалить элемент из кэша с помощью команды DEL, которая доступна через библиотеку StackExchange в качестве IDatabase.KeyDeleteAsync метода.

Когда Redis достигает предела памяти, он вытесняет ключи в соответствии с настроенной политикой вытеснения. Политика по умолчанию — это volatile-lruполитика, которая вытесняет наименее недавно использованный ключ с набором TTL. Другие политики включают allkeys-lru, volatile-randomи noeviction (что приводит к сбою операций записи при заполнении памяти). Выберите политику вытеснения в зависимости от того, использует ли приложение TTLs последовательно и предпочитаете ли вы защищать ключи без истечения срока действия. Дополнительные сведения см. в разделе "Управление памятью".

Перекрестное сопоставление кэшированных элементов

При кэшировании связанных элементов часто требуется найти их по связям, а не по первичному ключу. Например, вы можете кэшировать записи блога и отвечать на запросы, такие как "какие записи разделяют тег Y?" или "какие теги относятся к post X?"

В Управляемом Redis в Azure рекомендуется использовать RedisJSON и RediSearch. Сохраните каждый кэшированный элемент в виде документа JSON с его метаданными, а затем создайте индекс RediSearch по полям, которые необходимо запросить. RediSearch обрабатывает обратный поиск, фильтрацию на основе тегов, запросы диапазона и полнотекстовый поиск без необходимости поддерживать отдельные структуры индексов приложения.

Для более простых сценариев можно также использовать наборы Redis для создания прямых и обратных индексов вручную. Сохраните набор для каждого поста (содержащий его теги) и набор для каждого тега (содержащий идентификаторы постов):

foreach (BlogPost post in posts)
{
    string postTagsKey = $"blog:posts:{post.Id}:tags";
    await cache.SetAddAsync(
        postTagsKey, post.Tags.Select(s => (RedisValue)s).ToArray());

    foreach (var tag in post.Tags)
    {
        await cache.SetAddAsync($"tag:{tag}:blog:posts", post.Id);
    }
}

Затем можно запросить теги для публикации SetMembersAsync, найти общие теги между записями или SetCombineAsync(SetOperation.Intersect, ...)найти все записи для данного тега. Компромисс заключается в том, что приложение должно поддерживать как вперед, так и обратные наборы, что повышает сложность по мере роста числа связей.

Поиск недавно доступных элементов

Многие приложения должны отслеживать последние доступные или просматриваемые элементы. Например, сайт блога может отображать последние записи для возвращающегося посетителя. Списки Redis предоставляют эффективный способ реализации шаблонов кэширования, основанных на недавней информации. Элементы можно отправить в любой конец списка с помощью LPUSH или RPUSH, а удалить с помощью LPOP или RPOP. Используйте LTRIM для ограничения длины списка и предотвращения роста несвязанной памяти.

Построение таблицы лидеров

Отсортированные наборы Redis (ZSETs) поддерживают упорядоченные ранжирования путем связывания каждого элемента с числовым показателем. Redis автоматически сохраняет порядок. ZADD имеет сложность O(log N), а запросы диапазона, такие как ZRANGE и ZREVRANGE, имеют сложность O(log N + M), где M — количество возвращаемых элементов, поэтому отсортированные наборы остаются эффективными, даже если количество элементов велико.

Добавление элементов в список лидеров

В следующем примере показано, как добавить запись блога и её рейтинг в список лидеров с помощью команды ZADD через SortedSetAddAsync.

var db = connection.GetDatabase();
string redisKey = "blog:post_rankings";

BlogPost blogPost = ...; // The blog post being ranked

await db.SortedSetAddAsync(redisKey, blogPost.Title, blogPost.Score);
Получение ранжированных элементов

Элементы можно получить в порядке возрастания оценки с помощью SortedSetRangeByRankWithScoresAsync:

var entries = await db.SortedSetRangeByRankWithScoresAsync(redisKey);

foreach (var entry in entries)
{
    Console.WriteLine($"{entry.Element}: {entry.Score}");
}

Note

SortedSetRangeByRankAsync возвращает только значения элементов, а не оценки.

Получение элементов top-N

Чтобы получить элементы с наивысшей оценкой, такие как первые 10 записей, используйте порядок убывания:

foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(
                               redisKey, 0, 9, Order.Descending))
{
    Console.WriteLine(post);
}
Получение элементов по диапазону показателей

Вы также можете запрашивать элементы на основе границ оценки, а не ранжирования:

foreach (var post in await cache.SortedSetRangeByScoreWithScoresAsync(
                               redisKey, 5000, 100000))
{
    Console.WriteLine(post);
}

Чтобы предотвратить увеличение списка лидеров до бесконечности, удалите старые записи с помощью SortedSetRemoveRangeByRankAsync или используйте ключи, привязанные ко времени (например, ежедневные или еженедельные списки лидеров). Оценки можно обновлять атомарно с использованием SortedSetIncrementAsync (ZINCRBY).

Кэширование состояния сеанса и выходных данных HTML

Управляемый Redis azure можно использовать для хранения данных состояния сеанса и кэша выходных данных для приложений ASP.NET Core и ASP.NET. Сохраняя данные сеанса и выводимые данные в общем кэше на основе Redis, приложения, работающие в нескольких экземплярах, например, в Службе приложений Azure, Службе Azure Kubernetes (AKS), приложениях контейнеров Azure или масштабируемых наборах виртуальных машин, могут поддерживать последовательный пользовательский опыт без необходимости привязки к серверу.

Tip

Для повышения производительности разверните приложение и экземпляр Управляемого Redis Azure в том же регионе Azure.

ASP.NET Core

Приложения ASP.NET Core используют абстракцию IDistributedCache и посредническое программное обеспечение для сеанса. Управляемый Redis в Azure интегрируется с IDistributedCache через пакет Microsoft.Extensions.Caching.StackExchangeRedis.

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<your-cache-name>.<region>.redis.azure.net:10000";
    options.InstanceName = "app-cache:";
});

builder.Services.AddSession();

Промежуточное ПО кэширования вывода ASP.NET Core также может использовать Redis в качестве распределенного хранилища данных, позволяя приложениям совместно использовать сгенерированные фрагменты или страницы во всех экземплярах. Дополнительные сведения см. в разделе Поставщик кэша выходных данных ASP.NET Core для Redis.

Интеграция .NET Aspire

Приложения .NET Aspire могут использовать Aspire.Hosting.Azure.Redis пакет для объявления ресурса Azure управляемого Redis в хосте приложения. Потребляющие проекты автоматически получают конфигурацию подключения через внедрение зависимостей, что устраняет необходимость ручного управления строками подключения во всех службах.

// App host: declare the Azure Managed Redis resource
var cache = builder.AddAzureManagedRedis("cache");

builder.AddProject<Projects.ProductService>()
    .WithReference(cache);

Потребляющие службы регистрируют распределенный кэш так же, как и любой другой IDistributedCache поставщик. Дополнительные сведения см. в статье "Начало работы с интеграцией Redis".

Высокий уровень доступности, масштабируемость и секционирование

Каждый экземпляр Управляемого Redis в Azure использует первичную или реплику репликации. Служба отслеживает работоспособность узлов и автоматически переводит реплику, если основной выходит из строя. Так как репликация асинхронна, во время переключения на резервный ресурс может быть потеряно небольшое количество недавно записанных данных. Общие стратегии репликации, отработки отказа и многоуровневого кэширования см. в статье "Реализация высокого уровня доступности и масштабируемости" и повышение производительности в начале этой статьи.

Вы можете объединить локальный кэш в памяти с Управляемым Redis Azure, чтобы уменьшить задержку и предоставить резервный вариант, если общий кэш временно недоступен. Шаблон Circuit-Breaker и шаблон отложенного кэширования помогают управлять этим многоуровневым подходом.

Для рабочих нагрузок, превышающих емкость одного узла, Управляемый Redis Azure поддерживает секционирование (сегментирование) данных по нескольким узлам Redis. При использовании обеих политик кластеризации данные автоматически сегментируются между узлами с хэшированием "ключ-шард", автоматическим переключением на резервный узел и ресинхронизацией, а также онлайн ресшардированием (горизонтальное и вертикальное масштабирование). Управляемый Redis Azure поддерживает две политики кластеризации:

  • Политика кластеризации OSS (по умолчанию): Клиенты взаимодействуют напрямую с соответствующим сегментом и следуют семантике кластера Redis OSS, включая перенаправления MOVED и ASK. Клиенты с поддержкой кластера, такие как StackExchange.Redis, автоматически обрабатывают эти перенаправления. Эта политика обеспечивает наименьшую нагрузку на маршрутизацию.

  • Политика кластеризации Redis Enterprise: Прокси-сервер обеспечивает прозрачную маршрутизацию через одну конечную точку. Клиентам не нужно реализовать логику с поддержкой кластера или обрабатывать ответы MOVED/ASK. Эта политика обеспечивает более простую интеграцию клиентов, но представляет небольшую нагрузку на маршрутизацию.

Управляемый Redis Azure также поддерживает некластеризованный режим, который использует одну пару основной/реплика без шардинга. Этот режим подходит для небольших рабочих нагрузок, которые не требуют горизонтального масштабирования.

Note

Пользовательские модели разбиения (например, хэширование на стороне клиента или сторонние прокси-серверы) обычно нужны только в самостоятельно управляемых развертываниях Redis на виртуальных машинах или Kubernetes. Кластеризация управляемого Redis в Azure автоматически обрабатывает маршрутизацию, переключение при отказе и перераспределение шардов.

Активная георепликация

Для доступности в нескольких регионах Управляемый Redis azure поддерживает активную георепликацию, которая связывает экземпляры между регионами Azure в одну группу репликации. Каждый экземпляр может обрабатывать операции чтения и записи, а также автоматически синхронизировать изменения. Ваше приложение несет ответственность за перенаправление трафика на исправный экземпляр в случае регионального сбоя. Дополнительные сведения см. в разделе "Активная георепликация".

Сохраняемость данных

По умолчанию данные, кэшируемые в Azure Managed Redis, хранятся в памяти и могут быть утрачены при перезапуске узла или отработке отказа. Для рабочих нагрузок, в которых перестроение кэша из исходного хранилища данных будет медленным или дорогостоящим, Управляемый Redis Azure поддерживает необязательное сохранение данных:

  • Моментальные снимки RDB создаются периодически, фиксируя состояние на определенный момент времени, и сохраняются на управляемом диске. RDB имеет минимальное влияние на производительность во время обычных операций, но данные, записанные с момента последнего моментального снимка, могут быть потеряны.
  • AOF (Append-Only файл) протоколирует каждую операцию записи на диск. AOF снижает потенциальную потерю данных примерно на одну секунду операций записи, но создает большие файлы и может снизить пропускную способность записи.

Вы можете включить RDB и AOF вместе. Redis при запуске загружает моментальный снимок RDB, а затем воспроизводит журнал AOF для почти полного восстановления.

Это важно

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

Сведения о конфигурации см. в разделе "Настройка сохраняемости данных".

Защита кэшированных данных в Управляемом Redis в Azure

В руководстве по защищенным кэшируемым данным описываются проблемы управления доступом и передачи данных. Для управляемого Redis в Azure решаются следующие задачи:

  • Используйте проверку подлинности Идентификатора Microsoft Entra в качестве основного механизма управления доступом и следуйте принципу наименьших привилегий при предоставлении доступа.
  • Используйте частные конечные точки , чтобы ограничить сетевой доступ, чтобы трафик не проходит через общедоступный Интернет.
  • Управляемый Redis в Azure шифрует данные при передаче с помощью TLS и шифрует данные в состоянии покоя.

Рекомендации по сериализации

При хранении объектов .NET в Redis в качестве строковых значений их необходимо сериализовать. При выборе формата сериализации рассмотрите компромиссы между производительностью, взаимодействием, версиями и размером полезных данных. Для всех сценариев нет одного самого быстрого сериализатора. Тесты зависят от контекста и могут не отражать фактическую рабочую нагрузку.

Если уровень управляемого Redis Azure поддерживает RedisJSON, вы можете хранить объекты в виде нативных JSON-документов и запрашивать их отдельные поля без десериализации всего значения.

public static class RedisJsonExtensions
{
    public static async Task<T?> GetAsync<T>(
        this IDatabase cache,
        string key,
        string path = "$")
    {
        var result = await cache.ExecuteAsync("JSON.GET", key, path);

        if (result.IsNull)
            return default;

        return JsonSerializer.Deserialize<T>(result!);
    }

    public static async Task SetAsync<T>(
        this IDatabase cache,
        string key,
        T value,
        TimeSpan? expiry = null,
        string path = "$")
    {
        var json = JsonSerializer.Serialize(value);

        // Store JSON document
        await cache.ExecuteAsync("JSON.SET", key, path, json);

        // Apply TTL if provided
        if (expiry.HasValue)
        {
            await cache.KeyExpireAsync(key, expiry);
        }
    }

    public static async Task<bool> ExpireAsync(
        this IDatabase cache,
        string key,
        TimeSpan expiry)
    {
        return await cache.KeyExpireAsync(key, expiry);
    }
}

Вместо этого при сериализации значений в виде строк Redis используются распространенные варианты форматирования:

  • JSON — читаемая пользователем широкая кроссплатформенная поддержка. Не самый компактный формат, но хороший выбор при возврате кэшированных элементов непосредственно в HTTP-клиенты, так как он позволяет избежать дополнительного десериализации и повторной сериализации.

  • MessagePack — компактный двоичный формат без требования схемы. Создает несколько меньшие полезные данные, чем JSON, с меньшими затратами на сериализацию.

  • Protocol Buffers (protobuf) — это двоичный формат на основе схемы, который создает компактные нагрузки. Требуется .proto файлы определений и шаг компиляции для создания кода для конкретного языка.

  • BSON — двоичный формат, который расширяет json с дополнительными типами, такими как даты и необработанные двоичные данные. Полезные данные (объекты) сравнимы по размеру с JSON. Практический выбор, когда приложение уже использует BSON в другом месте, например с MongoDB.

Дальнейшие шаги

Следующие шаблоны также могут иметь отношение к вашему сценарию при реализации кэширования в приложениях:

  • Шаблон отложенного кэширования: этот шаблон описывает загрузку данных по запросу в кэш из хранилища данных. Этот шаблон также помогает обеспечить согласованность между данными, которые хранятся в кэше и данных в исходном хранилище данных.

  • Шаблон сегментирования содержит сведения о реализации горизонтальной секционирования, чтобы повысить масштабируемость при хранении и доступе к большим объемам данных.