Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Кэширование — это распространенный способ, который предназначен для повышения производительности и масштабируемости системы. Кэширование временно сохраняет часто используемые данные в хранилище, которое находится ближе к приложению, чем исходный источник данных. Этот подход может значительно повысить время отклика для клиентских приложений, обслуживая данные быстрее.
Кэширование наиболее эффективно, если экземпляр клиента многократно считывает одни и те же данные, особенно если все следующие условия применяются к исходному хранилищу данных:
- Хранилище данных остается относительно статическим.
- Оно является медленным по сравнению со скоростью кэша.
- Оно подвержено высокому уровню конкуренции.
- Это достаточно далеко от клиентов, что задержка сети является значительной.
Кэширование в распределенных приложениях
Распределенные приложения обычно реализуют один или оба из следующих стратегий при кэшировании данных:
Используйте частный кэш, где хранятся данные локально на компьютере, где выполняется приложение или служба.
Используйте общий кэш, который служит общим источником, к которому могут получить доступ несколько процессов и компьютеров.
В обоих случаях можно выполнять кэширование на стороне клиента и на стороне сервера. Процесс, предоставляющий пользовательский интерфейс для системы, например веб-браузер или классическое приложение, обрабатывает кэширование на стороне клиента. Кэширование на стороне сервера выполняется удаленно с помощью процесса, предоставляющего бизнес-службы.
Частное кэширование
Самый простой тип кэша — это хранилище в памяти. Он хранится в адресном пространстве одного процесса, а код, который выполняется в этом процессе, напрямую обращается к кэшу. Доступ к кэшу этого типа осуществляется быстро. Он также может предоставлять эффективные средства для хранения скромных объемов статических данных. Объем памяти, доступный на компьютере, обычно ограничивает размер кэша.
Если вам нужно кэшировать больше информации, чем физически возможно в памяти, можно записать кэшированные данные в локальную файловую систему. Доступ к данным из файловой системы занимает больше времени, чем если данные хранятся в памяти, но они по-прежнему должны быть более быстрыми и надежными, чем получение данных в сети.
Если у вас несколько экземпляров приложения, использующего эту модель одновременно, каждый экземпляр приложения имеет собственный независимый кэш, содержащий собственную копию данных.
Подумайте о кэше как моментальном снимке исходных данных в прошлом. Если эти данные не статически, скорее всего, разные экземпляры приложений содержат разные версии данных в своих кэшах. Таким образом, тот же запрос, выполняемый этими экземплярами, может возвращать различные результаты, как показано на следующей схеме.
Схема, показывающая несоответствие кэша в нескольких экземплярах приложений, подключенных к общей базе данных SQL. Справа находится база данных SQL, представленная в виде цилиндра. В левом верхнем углу находится большой круг с подписью «экземпляр приложения A», а в левом нижнем — круг такого же размера с подписью «экземпляр приложения B». Внутри экземпляра приложения A находится значок шестерёнки, обозначающий процесс приложения. Под значком шестерёнки находится таблица в виде сетки, представляющая кэш в оперативной памяти. За пределами границы есть надпись: «кэш — это моментальный снимок данных на момент X». Внутри экземпляра приложения B есть ещё один значок в виде шестерёнки. Под значком шестерёнки находится таблица в виде сетки, представляющая кэш в оперативной памяти. За пределами границы есть метка с текстом: «кэш — это снимок данных на момент Y». От базы данных SQL к экземпляру приложения A тянется линия с подписью: «экземпляр приложения A извлекает данные в момент X и кэширует их в памяти». Вторая строка расширяется от базы данных SQL к экземпляру приложения B, помеченный экземпляр приложения B извлекает данные во время Y и кэширует его в памяти. Между этими двумя линиями аннотации находится примечание о том, что информация в базе данных изменяется между моментами времени X и Y.
Общее кэширование
Используя общий кэш, вы можете убедиться, что все экземпляры приложений видят одинаковое представление кэшированных данных. Кэш расположен в отдельном расположении, которое обычно размещается в составе отдельной службы, как показано на следующей схеме.
Схема, показывающая, как служба общего кэша разрешает несоответствие кэша в нескольких экземплярах приложений, подключенных к базе данных SQL. В правом углу находится база данных SQL, представленная в виде цилиндра. В центре расположен блок с надписью "Служба общего кэша", содержащий таблицу в виде сетки, которая представляет общий кэш. В крайней левой части находится значок шестерёнки с подписью «экземпляр приложения A». Ниже расположен второй значок шестерёнки с подписью «экземпляр приложения B». Чёрная стрелка направлена от базы данных SQL к сетке службы общего кэша, показывая, что база данных заполняет общий кэш. От общей службы кэша отходят две синие стрелки: одна направлена к экземпляру приложения A, а другая — к экземпляру приложения B. Слева вертикально расположена фигурная скобка, охватывающая оба экземпляра приложения, рядом с которой находится подпись: «Оба экземпляра приложения видят одни и те же кэшированные данные». Эта метка и скобка выделяют ключевое преимущество архитектуры общего кэша: в отличие от кэша в памяти для каждого экземпляра, оба экземпляра приложения A и B извлекают данные из одного централизованного кэша.
Важное преимущество общего подхода к кэшированию — масштабируемость. Многие службы общего кэша реализуются с помощью кластера серверов и используют программное обеспечение для прозрачного распределения данных в кластере. Экземпляр приложения отправляет запрос в службу кэша. Базовая инфраструктура определяет расположение кэшированных данных в кластере. Вы можете легко масштабировать кэш, добавив дополнительные серверы.
Существует два основных недостатка общего подхода кэширования:
- Кэш медленнее для доступа, так как он больше не хранится локально с каждым экземпляром приложения.
- Реализация отдельной службы кэша может добавить сложность в решение.
Рекомендации по использованию кэширования
В следующих разделах описаны рекомендации по проектированию и использованию кэша.
Решите, когда кэшировать данные
Кэширование может значительно повысить производительность, масштабируемость и доступность. Чем больше данных, тем больше число пользователей, которым требуется доступ к этим данным, тем больше преимущества кэширования. Кэширование уменьшает задержку и состязание, связанные с обработкой больших объемов одновременных запросов в исходном хранилище данных.
Например, база данных может поддерживать ограниченное количество одновременных подключений. Получение данных из общего кэша, а не базовой базы данных, позволяет клиентскому приложению получить доступ к этим данным, даже если количество доступных подключений в настоящее время исчерпано. Кроме того, если база данных становится недоступной, клиентские приложения могут продолжать использовать данные в кэше.
Рассмотрите возможность кэширования данных, которые часто читаются, но редко изменяются. Например, кэшировать данные, имеющие более высокую долю операций чтения, чем операции записи. Однако не используйте кэш в качестве авторитетного хранилища критически важных сведений. Вместо этого сохраните все важные изменения в постоянном хранилище данных. Если кэш недоступен, приложение по-прежнему может работать с помощью хранилища данных, и вы не теряете важные сведения.
Определение эффективного кэширования данных
Чтобы эффективно использовать кэш, определите наиболее подходящие данные для кэширования и кэшируйте их в нужное время. Вы можете добавить данные в кэш, когда приложение получает его в первый раз. Приложение должно получить данные только один раз из хранилища данных, а затем последующий доступ может быть удовлетворен с помощью кэша.
Кроме того, можно частично или полностью заполнить кэш данными заранее, как правило, при запуске приложения. Этот подход называется сидингом. Однако, возможно, не рекомендуется реализовать инициализацию для крупного кэша, так как этот подход может привести к внезапно высокой нагрузке на исходное хранилище данных вскоре после запуска приложения.
Анализ шаблонов использования, чтобы решить, следует ли полностью или частично предварительно заполнить кэш, а также выбрать, какие данные следует кэшировать. Например, кэш можно заполнить статическими данными профиля пользователя для клиентов, которые используют приложение каждый день, но не для клиентов, которые используют приложение только один раз в неделю.
Кэширование обычно хорошо работает с данными, которые неизменяемы или изменяются редко. Примеры включают справочные сведения, такие как сведения о продукте и ценах в приложении электронной коммерции, или общие статические ресурсы, которые являются дорогостоящими для создания. Загрузите некоторые или все эти данные в кэш при запуске приложения, чтобы свести к минимуму спрос на ресурсы и повысить производительность. Также может понадобиться фоновый процесс, который периодически будет обновлять справочные данные в кэше, чтобы убедиться, что они актуальны. Или фоновый процесс может обновить кэш при изменении ссылочных данных.
Кэширование менее полезно для динамических данных, хотя существуют некоторые исключения. Дополнительные сведения см. в разделе "Кэш с высокой динамической динамикой данных " далее в этой статье. Когда исходные данные регулярно изменяются, кэшированная информация быстро устаревает, и накладные затраты на синхронизацию кэша с исходным хранилищем данных снижают эффективность кэширования.
Кэш не должен включать полные данные для сущности. Например, если элемент данных представляет многозначный объект, например банковский клиент с именем, адресом и балансом счета, некоторые из этих элементов могут оставаться статическими, такими как имя и адрес. Другие элементы, такие как баланс учетной записи, могут быть более динамичными. В таких ситуациях полезно кэшировать статические части данных и извлекать (или вычислять) только оставшуюся информацию при необходимости.
Выполните тестирование производительности и анализ использования, чтобы определить, подходит ли предварительное заполнение или загрузка кэша по запросу или сочетание обоих. Принимайте решение на основе изменчивости данных и характера их использования. Использование кэша и анализ производительности важны в приложениях, которые сталкиваются с тяжелыми нагрузками и должны быть высокомасштабируемыми. Например, в высокомасштабируемых сценариях можно заполнить кэш, чтобы уменьшить нагрузку на хранилище данных в пиковое время.
Кэширование также можно использовать для предотвращения повторяющихся вычислений во время работы приложения. Если операция преобразует данные или выполняет сложные вычисления, она может сохранить результаты операции в кэше. Если после этого требуется то же вычисление, приложение может получить результаты из кэша.
Приложение может изменять данные, которые размещаются в кэше. Однако думайте о кэше как временном хранилище данных, которое может исчезнуть в любое время. Не сохраняйте ценные данные только в кэше; Убедитесь, что данные также хранятся в исходном хранилище данных. Этот подход сводит к минимуму вероятность потери данных, если кэш становится недоступным.
Сохранение согласованности кэша при записи
Когда приложение изменяет данные, которые оно также кэширует, необходимо определить, как поддерживать согласованность кэша с основным хранилищем данных. Два подхода являются общими:
Инвалидация при записи: Запишите изменение в хранилище данных, а затем удалите затронутую запись кэша. Следующая операция чтения повторно заполнит кэш данными из хранилища данных. Этот подход упрощает путь записи, но читатель может столкнуться с отсутствием кэша или кратко видеть устаревшие данные сразу после записи. Шаблон Cache-Aside описывает этот подход.
Сквозная запись: Обновляйте хранилище данных и кэш в рамках одной операции записи и возвращайте ответ на запись только после того, как оба обновления будут успешно выполнены. Этот подход дает читателям обновленное значение сразу после успешной записи за счет более высокой задержки записи и большей логики координации.
Пример сквозного сценария, в котором Функции Azure используются для координации обновлений кэша по схеме write-through в База данных SQL Azure и Azure Managed Redis, см. в статье Кэширование по схеме write-through с Azure Managed Redis и База данных SQL Azure.
Используйте сквозную запись только для путей доступа с интенсивным чтением, где требуется актуальность данных сразу после записи. Для данных, которые редко считываются после записи или постоянно изменяются, отменяют запись или обход кэша.
Кэшировать высокодинамические данные
Хранение быстро изменяющихся данных в постоянном хранилище данных может наложить на систему дополнительные расходы. Например, рассмотрим устройство, которое постоянно сообщает о состоянии или другом измерении. Если приложение не кэширует эти данные на основе того, что кэшированные сведения обычно устарели, то при хранении и получении этих сведений из хранилища данных может быть верно то же самое. В течение времени, необходимого для сохранения и получения этих данных, это может измениться.
В такой ситуации рассмотрите преимущества хранения динамической информации непосредственно в кэше, а не в постоянном хранилище данных. ** Если данные некритичны и не требуют аудита, то не имеет значения, если случайные изменения будут утеряны.
Управление истечением срока действия данных в кэше
В большинстве случаев кэш содержит данные, которые являются копией данных из исходного хранилища данных. Данные в исходном хранилище данных могут измениться после его кэширования, что может сделать кэшированные данные устаревшими. Многие системы кэширования позволяют настроить срок действия кэша, что сокращает период, в течение которого данные могут быть устаревшими.
После истечения срока действия кэшированных данных кэш удаляет его. Затем приложение извлекает новые данные из исходного хранилища данных и может заменить просроченные данные в кэше. Политику истечения срока действия по умолчанию можно задать при настройке кэша. Во многих службах кэша можно также указать срок действия отдельных объектов при их хранении программным способом в кэше. Некоторые кэши позволяют указать срок действия как абсолютное значение или как скользящий срок действия, если к элементу не обращаются в течение указанного времени. Этот параметр переопределяет любую политику истечения срока действия кэша, но только для указанных объектов.
Note
Рассмотрим период истечения срока действия кэша и объектов, которые он содержит тщательно. Если сделать его слишком коротким, срок действия объектов истекает слишком быстро, и вы сокращаете преимущества использования кэша. Если вы делаете период слишком длинным, вы рискуете, что данные становятся устаревшими.
Если данные будут оставаться в кэше в течение длительного времени, кэш может заполниться. В этом случае любые запросы на добавление новых элементов в кэш могут привести к принудительному удалению некоторых элементов в процессе, известном как вытеснение. Службы кэша обычно вытесняют данные на основе принципа 'наименее недавно использованного' (LRU), но обычно можно изменить эту политику и предотвратить удаление элементов. Однако если вы используете этот подход, вы рискуете превысить объем памяти, доступной в кэше. Приложение, которое пытается добавить элемент в полный кэш, завершается сбоем с исключением.
Некоторые реализации кэширования могут предоставлять другие алгоритмы вытеснения. Типы политик вытеснения включают:
- Политика удаления наиболее недавно использованных данных: удаляет из кэша наиболее недавно использованные элементы, исходя из предположения, что эти данные больше не понадобятся.
- Политика FIFO («первым поступил — первым удалён»): в первую очередь удаляет из кэша самые старые данные.
- Явная политика удаления: удаляет элементы из кэша на основе события активации, например измененных данных.
Недопустимые данные в клиентском кэше
Данные, хранящиеся в клиентском кэше, считаются внешним контролем службы, которая предоставляет данные клиенту. Служба не может напрямую принудительно добавлять или удалять сведения из клиентского кэша.
Это ограничение означает, что клиент, использующий плохо настроенный кэш, может продолжать использовать устаревшие сведения. Например, если политики истечения срока действия кэша не реализованы должным образом, клиент может использовать устаревшие сведения, кэшированные локально при изменении информации в исходном источнике данных.
Если вы создаете веб-приложение, которое предоставляет данные по HTTP-соединению, вы можете косвенно заставить веб-клиент, например браузер или веб-прокси, запрашивать самую актуальную информацию. Это обновление можно активировать, изменив универсальный код ресурса (URI) при обновлении ресурса. Веб-клиенты обычно используют URI ресурса в качестве ключа в клиентском кэше, поэтому если URI изменяется, веб-клиент игнорирует все ранее кэшированные версии ресурса и получает новую версию.
Управление параллелизмом в кэше
Часто кэши проектируются для совместного использования несколькими экземплярами приложения. Каждый экземпляр приложения может считывать и изменять данные в кэше, поэтому те же проблемы с параллелизмом, возникающие с любым общим хранилищем данных, также применяются к кэшу. В ситуации, когда приложению необходимо изменить данные, удерживаемые в кэше, может потребоваться убедиться, что обновления, сделанные одним экземпляром приложения, не перезаписывают изменения, внесенные другим экземпляром.
В зависимости от характера данных и вероятности столкновений следует использовать один из двух подходов к параллелизму:
Оптимистичный: Перед обновлением данных приложение проверяет, изменились ли данные в кэше после получения. Если данные по-прежнему одинаковы, приложение вносит изменения. В противном случае приложение решает, следует ли обновить его. Бизнес-логика, которая управляет этим решением, зависит от приложения. Этот подход подходит для ситуаций, когда обновления нечасто или где столкновения вряд ли возникают.
Пессимистично: Когда приложение получает данные, он блокирует данные в кэше, чтобы предотвратить изменение другого экземпляра. Этот процесс гарантирует, что конфликты не возникают, но также может блокировать другие экземпляры, которым нужно обрабатывать те же данные. Пессимистичная параллельность может сказываться на масштабируемости решения и рекомендуется использовать только для краткосрочных операций. Этот подход может быть подходящим для ситуаций, когда столкновения более вероятны, особенно если приложение обновляет несколько элементов в кэше и должно обеспечить согласованность применения этих изменений.
Реализация высокой доступности и масштабируемости и повышение производительности
Избегайте использования кэша в качестве основного репозитория данных. Исходное хранилище данных, из которого заполняется кэш, служит этой ролью. Исходное хранилище данных гарантирует сохраняемость данных.
Будьте осторожны, чтобы не вводить критические зависимости от доступности общей службы кэша в ваших решениях. Приложение должно продолжать работу, если общий кэш недоступен. Приложение не должно становиться неотзывчивым или завершаться сбоем во время ожидания возобновления работы службы кэша.
Поэтому приложение должно быть готово к обнаружению доступности службы кэша и вернуться к исходному хранилищу данных, если кэш недоступен. ШаблонCircuit-Breaker полезен для обработки этого сценария. Службу, предоставляющую кэш, можно восстановить, и после того как она снова станет доступной, кэш можно повторно заполнить по мере считывания данных из исходного хранилища данных, следуя такой стратегии, как Cache-Aside pattern.
Однако масштабируемость системы может повлиять, если приложение вернется к исходному хранилищу данных, когда кэш временно недоступен. Пока кэш восстанавливается, основное хранилище данных может быть перегружено запросами данных, что вызывает тайм-ауты и сбои соединения.
Рассмотрите возможность реализации локального частного кэша в каждом экземпляре приложения вместе с общим кэшем, к которому обращаются все экземпляры приложения. Когда приложение получает элемент, он может сначала проверить его в локальном кэше, а затем в общем кэше и, наконец, в исходном хранилище данных. Локальный кэш можно заполнить с помощью данных в общем кэше или в базе данных, если общий кэш недоступен.
Этот подход требует тщательной настройки, чтобы предотвратить устаревание локального кэша по сравнению с общим кэшем. Однако локальный кэш выступает в качестве буфера, если общий кэш недоступен, как показано на следующей схеме.
Для поддержки больших кэшей, которые содержат относительно долгоживущие данные, некоторые службы кэша предоставляют высокую доступность, которая реализует автоматическое переключение при сбое в случае, если кэш становится недоступен. Этот подход обычно включает копирование кэшированных данных, хранящихся на сервере первичного кэша, на сервер вторичного кэша и переключение на вторичный сервер, если первичный сервер выходит из строя или теряется соединение.
Чтобы уменьшить задержку, возникающую при записи в несколько мест, репликация на вторичный сервер может выполняться асинхронно при записи данных в кэш на основном сервере. Такой подход приводит к тому, что некоторые кэшированные сведения могут быть потеряны при сбое, но доля этих данных должна быть небольшой, по сравнению с общим размером кэша.
Если общий кэш велик, может оказаться полезным секционировать кэшированные данные по узлам, чтобы снизить вероятность возникновения спорных ситуаций и повысить масштабируемость. Многие общие кэши поддерживают возможность динамического добавления и удаления узлов и перебалансирования данных между секциями. Этот подход может включать кластеризацию, в котором коллекция узлов представлена клиентским приложениям в виде одного кэша. Внутри системы, данные распределяются между узлами по предопределенной стратегии распределения, равномерно распределяя нагрузку. Дополнительные сведения см. в шаблоне сегментирования.
Кластеризация также может увеличить доступность кэша. Если узел выйдет из строя, оставшаяся часть кэша по-прежнему доступна. Кластеризация часто используется с репликацией и отказоустойчивостью. Каждый узел можно реплицировать, и реплика может быть быстро введена в эксплуатацию, если узел выходит из строя.
Многие операции чтения и записи, скорее всего, связаны с одними значениями данных или объектами. Однако иногда может потребоваться хранить или извлекать большие объемы данных. Например, создание кэша может включать запись сотен или тысяч элементов в кэш. Кроме того, приложению может потребоваться получить большое количество связанных элементов из кэша в рамках того же запроса.
Многие крупномасштабные кэши предоставляют пакетные операции для этих целей. Эта функция позволяет клиентскому приложению упаковать большой объем элементов в один запрос, что снижает затраты на выполнение большого количества небольших запросов.
Кэширование и итоговая согласованность
Для работы шаблона Cache-Aside экземпляр приложения, заполняющего кэш, должен иметь доступ к последней и согласованной версии данных. В системе, реализующей итоговую согласованность (например, реплицированное хранилище данных), это условие может не быть истинным.
Один экземпляр приложения может изменить элемент данных и сделать кэшированную версию этого элемента недействительным. Другой экземпляр приложения может попытаться прочитать этот элемент из кэша, что приводит к пропуску кэша. Затем он считывает данные из хранилища данных и добавляет его в кэш. Однако если хранилище данных не полностью синхронизировано с другими репликами, экземпляр приложения может считывать и заполнять кэш старым значением.
Распределенный кэш представляет еще один уровень этой проблемы. Теорема CAP утверждает, что распределенная система может обеспечить только два из трех гарантий: согласованность, доступность и устойчивость к разделениям. Так как сетевые секции неизбежны в облачных средах, необходимо выбрать между согласованностью и доступностью. Большинство распределенных кэшей, включая Redis, отдают приоритет доступности и устойчивости разделов в ущерб строгой согласованности. Этот приоритет означает, что операции чтения из реплики кэша могут возвращать устаревшие данные во время разделения сети или сразу после записи на другом узле. При разработке стратегии кэширования определите, какой уровень устаревания данных может допустить ваше приложение, и соответствующим образом задайте значения времени жизни (TTL). Для данных, которые должны быть текущими, используйте более короткие списки TTL или обходить кэш полностью и считывать из исходного хранилища данных.
Дополнительные сведения об обработке согласованности данных в распределенных системах см. в рекомендациях по обработке микрослужб.
Защита кэшированных данных
Независимо от используемой службы кэша рекомендуется защитить данные в кэше от несанкционированного доступа. Две основные проблемы:
- Конфиденциальность данных в кэше.
- Конфиденциальность данных по мере перемещения между кэшем и приложением, которое его использует.
Для защиты данных в кэше служба кэша может реализовать механизм проверки подлинности, требующий от приложений указания следующих сведений:
- Какие идентификации могут получить доступ к данным в кэше.
- Какие операции чтения и записи разрешено выполнять этим субъектам.
Чтобы сократить накладные расходы, связанные с чтением и записью данных, после того как субъекту предоставлены права на запись в кэш или чтение из него, этот субъект может использовать любые данные в кэше.
Если необходимо ограничить доступ к подмножествам кэшированных данных, используйте один из следующих подходов:
Разделение кэша на секции с помощью разных серверов кэша. Предоставляйте идентификаторам доступ только к тем разделам, которые им разрешено использовать.
Шифрование данных в каждом подмножестве с помощью разных ключей. Предоставляйте ключи шифрования только тем сущностям, которые должны иметь доступ к каждому подмножеству. Клиентское приложение по-прежнему может получить все данные в кэше, но оно может расшифровать только данные, для которых у него есть ключи.
Кроме того, необходимо защитить данные в процессе их поступления в кэш и выхода из него. Зависит от функций безопасности, предоставляемых сетевой инфраструктурой, используемой клиентскими приложениями для подключения к кэшу. Если кэш реализуется с помощью локального сервера в той же организации, где размещаются клиентские приложения, изоляция самой сети может не потребовать дополнительных действий. Если кэш находится удаленно и требует подключения TCP или HTTP через общедоступную сеть, например Интернет, рассмотрите возможность реализации TLS.
Реализация кэширования с помощью Azure Managed Redis
В остальных разделах этой статьи описывается реализация шаблонов кэширования с помощью Azure Managed Redis. Управляемый сервис Redis в Azure — это управляемая служба Redis, которую можно использовать в качестве общего кэша для экземпляров приложений. Он поддерживает кэширование значений ключа, структуры данных, такие как наборы, отсортированные наборы и списки, а также необязательное сохранение устойчивости во время перезапусков.
Сведения о доступных уровнях, планировании емкости, сети и компонентах см. в документации по Управляемому Redis в Azure.
Подключение и настройка клиентских приложений
Redis поддерживает клиентские приложения на многих языках программирования. Для приложений .NET доступны несколько клиентских библиотек, которые подходят для разных рабочих нагрузок Redis. Выбор библиотеки зависит от того, используете ли Redis строго в качестве кэша или в качестве платформы данных с несколькими моделями.
Чтобы подключиться к серверу Redis, используйте статический Connect метод ConnectionMultiplexer класса. Соединение, которое создает этот метод, создается для использования на протяжении всего времени существования клиентского приложения. Несколько параллельных потоков могут использовать одно и то же подключение. Не подключайтесь и не отключайтесь каждый раз при выполнении операции Redis, так как это может снизить производительность.
Примеры подключения для конкретного языка см. в разделе Use Azure Managed Redis в .NET Core.
Выбор клиентской библиотеки .NET
При использовании Azure Managed Redis для кэширования используйте следующие библиотеки .NET:
StackExchange.Redis: Низкоуровневый клиент Redis, обеспечивающий высокую производительность. Используйте его, если вам нужен прямой доступ к командам Redis, атомарным операциям, транзакциям, конвейеру или скрипту Lua.
Microsoft.Extensions.Caching.StackExchangeRedis: Предоставляет интеграцию с ASP.NET Core
IDistributedCache. Используйте его для простого кэширования пар «ключ-значение», где значения хранятся как неявные байтовые массивы. Эта абстракция не предоставляет расширенные структуры данных Redis.
Эти библиотеки предоставляют примитивы, необходимые для создания общих шаблонов кэширования, но приложение должно реализовать саму логику кэширования.
Реализация шаблонов кэширования
Самый простой способ использования Redis для кэширования заключается в хранении значений под ключами с помощью модели "ключ-значение". Значения могут быть строками или двоичными данными произвольной длины, что позволяет Redis хорошо подходит для кэширования сериализованных объектов, данных конфигурации, состояния сеанса или предварительно вычисляемых результатов.
Тщательно спроектируйте пространство ключей и используйте значимые (но не излишне длинные) ключи. Например, используйте структурированные ключи, например customer:100 (а не просто 100), чтобы представить ключ для клиента с идентификатором 100. Эта схема позволяет различать значения, которые хранят разные типы данных. Например, можно также использовать ключ orders:100 для обозначения заказа с идентификатором 100.
Хотя строки являются наиболее распространенным подходом к кэшированию, Redis поддерживает широкий набор собственных типов данных, таких как хэши, списки, наборы, отсортированные наборы и потоки, которые позволяют более гибким шаблонам кэширования. Дополнительные сведения о типах данных Redis см. в документации Redis по типам данных.
Реализация шаблона Cache-Aside
Как описано в разделе "Определение эффективного кэширования данных", распространенный подход заключается в загрузке данных в кэш по запросу. В следующем примере сначала проверяется кэш, при отсутствии данных они извлекаются из источника и результат сохраняется для последующих запросов.
var config = new ConfigurationOptions();
// ... configure endpoint, credentials, TLS, etc.
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase cache = redisHostConnection.GetDatabase();
async Task<string> RetrieveItemAsync(string itemKey)
{
// Attempt to retrieve the item from the Redis cache
string itemValue = await cache.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 cache.StringSetAsync(itemKey, itemValue);
}
return itemValue;
}
Выполнение атомарных и пакетных операций
Если несколько клиентов или экземпляров приложений совместно используют кэш, необходимо предотвратить одновременное обновление данных, чтобы избежать их повреждения. Общие стратегии параллелизма описаны в разделе "Управление параллелизмом в кэше " ранее в этой статье. Redis предоставляет несколько механизмов реализации этих стратегий.
Атомарные операции с одним ключом: Используйте команды, которые обновляют значение за один шаг, устраняя состояния гонки, возникающие, когда
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, 9, 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
Можно использовать Azure Managed Redis для хранения данных о состоянии сеанса и кэше выходных данных для ASP.NET Core и ASP.NET приложений. Если хранить данные сеанса и отрендеренные выходные данные в общем кэше на основе Redis, приложения, работающие в нескольких экземплярах, например в Служба приложений Azure, Azure Kubernetes Service (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 и паттерн Cache-Aside помогают управлять этим многоуровневым подходом.
Для рабочих нагрузок, превышающих емкость одного узла, Управляемый Redis Azure поддерживает секционирование (сегментирование) данных по нескольким узлам Redis. При использовании обеих политик кластеризации данные автоматически распределяются между узлами с помощью хеширования ключей для распределения по сегментам, автоматического переключения при отказе и ресинхронизации, а также онлайн-перераспределения сегментов (горизонтального масштабирования и уменьшения масштаба). Управляемый Redis Azure поддерживает две политики кластеризации:
Политика кластеризации OSS (по умолчанию): Клиенты взаимодействуют напрямую с соответствующим сегментом и следуют семантике кластера Redis OSS, включая перенаправления MOVED и ASK. Клиенты с поддержкой кластера, такие как StackExchange.Redis, автоматически обрабатывают эти перенаправления. Эта политика обеспечивает наименьшую нагрузку на маршрутизацию.
Политика кластеризации Redis Enterprise: Прокси-сервер обеспечивает прозрачную маршрутизацию через одну конечную точку. Клиентам не нужно реализовать логику с поддержкой кластера или обрабатывать ответы MOVED/ASK. Эта политика обеспечивает более простую интеграцию клиента, но представляет небольшое количество затрат на маршрутизацию.
Azure Managed Redis также поддерживает режим некластерный режим, в котором используется одна пара «основной узел — реплика» без шардирования. Этот режим подходит для небольших рабочих нагрузок, которые не требуют горизонтального масштабирования.
Note
Пользовательские модели секционирования, такие как хэширование на стороне клиента или прокси, отличные от Microsoft, обычно требуются только в развертываниях Redis с самостоятельным управлением на виртуальных машинах или в Kubernetes. Кластеризация управляемого Redis в Azure автоматически обрабатывает маршрутизацию, переключение при отказе и перераспределение шардов.
Активная георепликация
Для обеспечения доступности в нескольких регионах Azure Managed Redis поддерживает активную георепликацию, которая объединяет экземпляры в разных регионах Azure в единую группу репликации. Каждый экземпляр может обрабатывать операции чтения и записи, а также автоматически синхронизировать изменения. Ваше приложение несет ответственность за перенаправление трафика на исправный экземпляр в случае регионального сбоя. Дополнительные сведения см. в разделе "Настройка активной георепликации".
Сохраняемость данных
По умолчанию данные, кэшируемые в Azure Managed Redis, хранятся в памяти и могут быть утрачены при перезапуске узла или отработке отказа. Для рабочих нагрузок, в которых перестроение кэша из исходного хранилища данных будет медленным или дорогостоящим, Управляемый Redis Azure поддерживает необязательное сохранение данных:
Снимки RDB (базы данных Redis) создают периодические снимки состояния на определенный момент времени, которые сохраняются на управляемом диске. RDB имеет минимальный эффект производительности во время обычных операций, но данные, записанные с момента последнего моментального снимка, могут быть потеряны.
AOF (Append-Only файл) протоколирует каждую операцию записи на диск. AOF снижает потенциальную потерю данных примерно на одну секунду операций записи, но создает большие файлы и может снизить пропускную способность записи.
RDB и AOF можно использовать вместе. Redis при запуске загружает моментальный снимок RDB, а затем воспроизводит журнал AOF для почти полного восстановления.
Important
Персистентность повышает устойчивость к отказам узлов, но сама по себе не является механизмом резервного копирования или аварийного восстановления. Для критически важных данных всегда сохраняйте авторитетную копию в исходном хранилище данных и используйте шаблонCache-Aside для повторного копирования кэша.
Сведения о конфигурации см. в разделе "Настройка сохраняемости данных".
Защита кэшированных данных в Управляемом Redis в Azure
В руководстве по защищенным кэшируемым данным описываются проблемы управления доступом и передачи данных. Azure Managed Redis помогает решить эти проблемы следующими способами:
Используйте проверку подлинности Идентификатора 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.
Связанные ресурсы
- Документация по Управляемому Redis в Azure
- Часто задаваемые вопросы об Управляемом Redis в Azure
- Документация по Redis
- StackExchange.Redis
При реализации кэширования в приложениях могут быть важны следующие шаблоны:
Шаблон Cache-Aside: Этот шаблон описывает, как по запросу загружать данные из хранилища данных в кэш. Она также помогает обеспечить согласованность данных в кэше и данных в исходном хранилище данных.
Шаблон сегментирования. Этот шаблон предоставляет сведения о реализации горизонтальной секционирования, чтобы повысить масштабируемость при хранении и доступе к большим объемам данных.