Кэширование — это распространенный способ, который предназначен для повышения производительности и масштабируемости системы. Он кэширует данные, временно скопировав часто доступ к данным в быстрое хранилище, которое находится рядом с приложением. Если это быстрое хранилище данных находится ближе к приложению, чем исходный оригинал, кэширование может значительно улучшить время отклика для клиентских приложений путем более быстрой обработки данных.
Кэширование наиболее эффективно, если экземпляр клиента многократно считывает одни и те же данные, особенно если все следующие условия применяются к исходному хранилищу данных:
- Он остается относительно статическим.
- Оно является медленным по сравнению со скоростью кэша.
- Оно является объектом высокого числа конкурентных запросов.
- Это далеко, когда задержка сети может привести к замедлению доступа.
Кэширование в распределенных приложениях
Распределенные приложения обычно реализуют один или оба из следующих стратегий при кэшировании данных:
- Они используют частный кэш, где данные хранятся локально на компьютере, на котором выполняется экземпляр приложения или службы.
- Они используют общий кэш, выступая в качестве общего источника, доступ к которому можно получить несколькими процессами и компьютерами.
В обоих случаях кэширование может выполняться на стороне клиента и на стороне сервера. Кэширование на стороне клиента выполняется процессом, предоставляющим пользовательский интерфейс для системы, например веб-браузер или классическое приложение. Кэширование на стороне сервера выполняется процессом, предоставляющим бизнес-службы, работающие удаленно.
Частное кэширование
Самый простой тип кэша — это хранилище в памяти. Он хранится в адресном пространстве одного процесса и обращается непосредственно к коду, который выполняется в этом процессе. Доступ к этому типу кэша выполняется быстро. Он также может предоставлять эффективные средства для хранения скромных объемов статических данных. Размер кэша обычно ограничивается объемом памяти, доступной на компьютере, на котором размещен процесс.
Если вам нужно кэшировать больше информации, чем физически возможно в памяти, можно записать кэшированные данные в локальную файловую систему. Этот процесс будет медленнее получать доступ, чем данные, которые хранятся в памяти, но все равно должны быть более быстрыми и надежными, чем получение данных в сети.
Если у вас несколько экземпляров приложения, использующего эту модель одновременно, каждый экземпляр приложения имеет собственный независимый кэш, содержащий собственную копию данных.
Подумайте о кэше как моментальном снимке исходных данных в прошлом. Если эти данные не статически, скорее всего, разные экземпляры приложений содержат разные версии данных в своих кэшах. Таким образом, один и тот же запрос, выполняемый этими экземплярами, может возвращать различные результаты, как показано на рис. 1.
Рис. 1. Использование кэша в памяти в разных экземплярах приложения.
Общий кэширование
Если вы используете общий кэш, это может помочь устранить проблемы, которые могут отличаться в каждом кэше, что может произойти при кэшировании в памяти. Совместное кэширование гарантирует, что разные экземпляры приложений видят одинаковое представление кэшированных данных. Он находит кэш в отдельном расположении, которое обычно размещается в составе отдельной службы, как показано на рис. 2.
Рис. 2. Использование общего кэша.
Важное преимущество общего подхода к кэшированию — это масштабируемость, которая она предоставляет. Многие службы общего кэша реализуются с помощью кластера серверов и используют программное обеспечение для прозрачного распределения данных в кластере. Экземпляр приложения просто отправляет запрос в службу кэша. Базовая инфраструктура определяет расположение кэшированных данных в кластере. Вы можете легко масштабировать кэш, добавив дополнительные серверы.
Существует два основных недостатка общего подхода кэширования:
- Кэш медленнее для доступа, так как он больше не хранится локально для каждого экземпляра приложения.
- Требование реализации отдельной службы кэша может добавить сложность в решение.
Рекомендации по использованию кэширования
В следующих разделах подробно описаны рекомендации по проектированию и использованию кэша.
Решите, когда кэшировать данные
Кэширование может значительно повысить производительность, масштабируемость и доступность. Чем больше данных у вас есть, тем больше число пользователей, которым требуется доступ к этим данным, тем больше преимущества кэширования становятся. Кэширование уменьшает задержку и состязание, связанное с обработкой больших объемов одновременных запросов в исходном хранилище данных.
Например, база данных может поддерживать ограниченное количество одновременных подключений. Получение данных из общего кэша, а не базовой базы данных, позволяет клиентскому приложению получить доступ к этим данным, даже если количество доступных подключений в настоящее время исчерпано. Кроме того, если база данных становится недоступной, клиентские приложения могут продолжать использовать данные, которые хранятся в кэше.
Рассмотрите возможность кэширования данных, которые часто читаются, но редко изменяются (например, данные, имеющие более высокую долю операций чтения, чем операции записи). Однако мы не рекомендуем использовать кэш в качестве авторитетного хранилища критически важных сведений. Вместо этого убедитесь, что все изменения, которые приложение не может позволить себе потерять, всегда сохраняются в постоянном хранилище данных. Если кэш недоступен, приложение по-прежнему может работать с помощью хранилища данных, и вы не потеряете важные сведения.
Определение эффективного кэширования данных
Ключ к использованию кэша заключается в определении наиболее подходящих данных для кэширования и кэшировании в соответствующее время. Данные можно добавить в кэш по запросу при первом получении приложением. Приложение должно получить данные только один раз из хранилища данных, а последующий доступ может быть удовлетворен с помощью кэша.
Кроме того, кэш может быть частично или полностью заполнен данными заранее, как правило, при запуске приложения (подход, известный как заполнение). Однако, возможно, не рекомендуется реализовать начальное значение для большого кэша, так как этот подход может привести к внезапной высокой нагрузке на исходное хранилище данных при запуске приложения.
Часто анализ шаблонов использования помогает решить, следует ли полностью или частично предварительно заполнить кэш, а также выбрать данные для кэширования. Например, можно заполнить кэш статическими данными профиля пользователя для клиентов, которые регулярно используют приложение (возможно, каждый день), но не для клиентов, которые используют приложение только один раз в неделю.
Кэширование обычно хорошо работает с данными, неизменяемыми или изменяющимися редко. Примеры включают справочные сведения, такие как сведения о продукте и ценах в приложении электронной коммерции, или общие статические ресурсы, которые являются дорогостоящими для создания. Некоторые или все эти данные можно загрузить в кэш при запуске приложения, чтобы свести к минимуму спрос на ресурсы и повысить производительность. Кроме того, может потребоваться фоновый процесс, который периодически обновляет эталонные данные в кэше, чтобы убедиться, что он up-to-date. Или фоновый процесс может обновить кэш при изменении ссылочных данных.
Кэширование менее полезно для динамических данных, хотя в этом разделе рассматриваются некоторые исключения (дополнительные сведения см. в разделе "Кэш высокодинамическая информация"). Когда исходные данные регулярно изменяются, кэшированные данные становятся устаревшими или накладными затратами на синхронизацию кэша с исходным хранилищем данных снижает эффективность кэширования.
Кэш не должен включать полные данные для сущности. Например, если элемент данных представляет многозначный объект, например банковский клиент с именем, адресом и балансом счета, некоторые из этих элементов могут оставаться статическими, такими как имя и адрес. Другие элементы, такие как баланс учетной записи, могут быть более динамичными. В таких ситуациях может быть полезно кэшировать статические части данных и извлекать (или вычислять) только оставшиеся сведения, если это необходимо.
Рекомендуется выполнить тестирование производительности и анализ использования, чтобы определить, подходит ли предварительное заполнение или загрузка кэша по запросу или сочетание обоих. Решение должно основываться на модели волатильности и использования данных. Использование кэша и анализ производительности важны в приложениях, которые сталкиваются с тяжелыми нагрузками и должны быть высокомасштабируемыми. Например, в высокомасштабируемых сценариях можно заполнить кэш, чтобы уменьшить нагрузку на хранилище данных в пиковое время.
Кэширование также можно использовать для предотвращения повторяющихся вычислений во время работы приложения. Если операция преобразует данные или выполняет сложные вычисления, она может сохранить результаты операции в кэше. Если после этого требуется то же вычисление, приложение может просто получить результаты из кэша.
Приложение может изменять данные, которые хранятся в кэше. Однако мы рекомендуем думать о кэше как временном хранилище данных, которое может исчезнуть в любое время. Не сохраняйте ценные данные только в кэше; Убедитесь, что данные также хранятся в исходном хранилище данных. Это означает, что если кэш становится недоступным, можно свести к минимуму вероятность потери данных.
Кэшировать динамические данные
При быстром изменении информации в постоянном хранилище данных она может наложить на систему издержки. Например, рассмотрим устройство, которое постоянно сообщает о состоянии или другом измерении. Если приложение не кэширует эти данные на основе того, что кэшированные сведения почти всегда будут устаревшими, то при хранении и получении этих данных из хранилища данных может быть верно то же самое. В течение времени, необходимого для сохранения и получения этих данных, возможно, он изменился.
В такой ситуации рассмотрите преимущества хранения динамической информации непосредственно в кэше, а не в постоянном хранилище данных. Если данные некритичные и не требуют аудита, это не имеет значения, если случайные изменения потеряны.
Управление истечением срока действия данных в кэше
В большинстве случаев данные, содержащиеся в кэше, являются копией данных, которые хранятся в исходном хранилище данных. Данные в исходном хранилище данных могут измениться после его кэширования, что приведет к тому, что кэшированные данные становятся устаревшими. Во многих системах кэширования можно настроить срок действия кэша и сократить период, в течение которого данные могут быть устаревшими.
Когда срок действия кэшированных данных истекает, он удаляется из кэша, и приложение должно получить данные из исходного хранилища данных (он может поместить только что полученные данные в кэш). Политику истечения срока действия по умолчанию можно задать при настройке кэша. Во многих службах кэша можно также указать срок действия отдельных объектов при их хранении программным способом в кэше. Некоторые кэши позволяют указать срок действия как абсолютное значение или как скользящее значение, которое приводит к удалению элемента из кэша, если доступ к нему не выполняется в течение указанного времени. Этот параметр переопределяет любую политику истечения срока действия кэша, но только для указанных объектов.
Замечание
Рассмотрим период истечения срока действия кэша и объектов, которые он содержит тщательно. Если сделать его слишком коротким, срок действия объектов истекает слишком быстро, и вы уменьшите преимущества использования кэша. Если вы делаете период слишком длинным, вы рискуете, что данные становятся устаревшими.
Также возможно, что кэш может заполниться, если данные могут оставаться резидентными в течение длительного времени. В этом случае любые запросы на добавление новых элементов в кэш могут привести к принудительному удалению некоторых элементов в процессе, известном как вытеснение. Службы кэша обычно вытеснили данные по крайней мере недавно использованной (LRU), но обычно можно переопределить эту политику и предотвратить вытеснение элементов. Однако если вы используете этот подход, вы рискуете превысить объем памяти, доступной в кэше. Приложение, которое пытается добавить элемент в кэш, завершится сбоем с исключением.
Некоторые реализации кэширования могут предоставлять дополнительные политики вытеснения. Существует несколько типов политик вытеснения. К ним относятся:
- Последняя политика (в ожидании того, что данные не потребуются снова).
- Политика первого выхода (самые старые данные вытеснили сначала).
- Явная политика удаления на основе триггерного события (например, измененных данных).
Недопустимые данные в клиентском кэше
Данные, хранящиеся в клиентском кэше, обычно считаются внешними под эгидой службы, которая предоставляет данные клиенту. Служба не может напрямую принудительно добавлять или удалять сведения из клиентского кэша.
Это означает, что клиент, использующий плохо настроенный кэш, может продолжать использовать устаревшие сведения. Например, если политики истечения срока действия кэша не реализованы должным образом, клиент может использовать устаревшие сведения, кэшированные локально при изменении сведений в исходном источнике данных.
Если вы создаете веб-приложение, которое обслуживает данные через HTTP-подключение, вы можете неявно принудительно принудительно принудить веб-клиент (например, браузер или веб-прокси) получить последние сведения. Это можно сделать, если ресурс обновляется изменением URI этого ресурса. Веб-клиенты обычно используют URI ресурса в качестве ключа в клиентском кэше, поэтому если URI изменяется, веб-клиент игнорирует все ранее кэшированные версии ресурса и получает новую версию.
Управление параллелизмом в кэше
Кэши часто предназначены для совместного использования несколькими экземплярами приложения. Каждый экземпляр приложения может считывать и изменять данные в кэше. Следовательно, те же проблемы параллелизма, возникающие при любом общем хранилище данных, также применяются к кэшу. В ситуации, когда приложению необходимо изменить данные, которые хранятся в кэше, может потребоваться убедиться, что обновления, внесенные одним экземпляром приложения, не перезаписывают изменения, внесенные другим экземпляром.
В зависимости от характера данных и вероятности конфликтов можно применить один из двух подходов к параллелизму:
- Оптимистичный. Непосредственно перед обновлением данных приложение проверяет, изменились ли данные в кэше после получения. Если данные по-прежнему одинаковы, можно внести изменения. В противном случае приложению необходимо решить, следует ли обновить его. (Бизнес-логика, которая управляет этим решением, будет конкретным приложением.) Этот подход подходит для ситуаций, когда обновления нечасто или где столкновения вряд ли возникают.
- Пессимистично. При получении данных приложение блокирует его в кэше, чтобы предотвратить изменение другого экземпляра. Этот процесс гарантирует, что столкновения не могут возникать, но они также могут блокировать другие экземпляры, которые должны обрабатывать те же данные. Пессимистичное параллелизм может повлиять на масштабируемость решения и рекомендуется использовать только для краткосрочных операций. Этот подход может быть подходящим для ситуаций, когда столкновения более вероятны, особенно если приложение обновляет несколько элементов в кэше и должно обеспечить согласованность применения этих изменений.
Реализация высокой доступности и масштабируемости и повышение производительности
Избегайте использования кэша в качестве основного репозитория данных; это роль исходного хранилища данных, из которого заполняется кэш. Исходное хранилище данных отвечает за сохранение данных.
Будьте осторожны, чтобы не вводить критические зависимости от доступности общей службы кэша в ваших решениях. Приложение должно продолжать работу, если служба, предоставляющая общий кэш, недоступна. Приложение не должно перестать отвечать на запросы или завершать сбой при ожидании возобновления службы кэша.
Поэтому приложение должно быть готово к обнаружению доступности службы кэша и вернуться к исходному хранилищу данных, если кэш недоступен. ШаблонCircuit-Breaker полезен для обработки этого сценария. Служба, предоставляющая кэш, может быть восстановлена, и после того, как она станет доступной, кэш может быть повторен, так как данные считываются из исходного хранилища данных, следуя стратегии, такой как шаблон "Кэш в сторону".
Однако масштабируемость системы может повлиять, если приложение возвращается в исходное хранилище данных, когда кэш временно недоступен. В то время как хранилище данных восстанавливается, исходное хранилище данных может быть заболочены запросами на данные, в результате чего время ожидания и неудачные подключения.
Рассмотрите возможность реализации локального частного кэша в каждом экземпляре приложения вместе с общим кэшем, к которому обращаются все экземпляры приложения. Когда приложение получает элемент, он может сначала проверить его в локальном кэше, а затем в общем кэше и, наконец, в исходном хранилище данных. Локальный кэш можно заполнить с помощью данных в общем кэше или в базе данных, если общий кэш недоступен.
Этот подход требует тщательной настройки, чтобы предотвратить слишком устаревший локальный кэш в отношении общего кэша. Однако локальный кэш выступает в качестве буфера, если общий кэш недоступен. На рисунке 3 показана эта структура.
Рис. 3. Использование локального частного кэша с общим кэшем.
Для поддержки больших кэшей, которые содержат относительно длительные данные, некоторые службы кэша предоставляют высокий уровень доступности, который реализует автоматическую отработку отказа, если кэш становится недоступным. Этот подход обычно включает репликацию кэшированных данных, хранящихся на сервере первичного кэша на сервер вторичного кэша, и переключение на дополнительный сервер, если первичный сервер завершается сбоем или подключением теряется.
Чтобы уменьшить задержку, связанную с записью в несколько назначений, репликация на дополнительный сервер может происходить асинхронно при записи данных в кэш на основном сервере. Такой подход приводит к тому, что некоторые кэшированные сведения могут быть потеряны при сбое, но доля этих данных должна быть небольшой, по сравнению с общим размером кэша.
Если общий кэш велик, может оказаться полезным секционировать кэшированные данные по узлам, чтобы снизить вероятность возникновения спорных ситуаций и повысить масштабируемость. Многие общие кэши поддерживают возможность динамического добавления (и удаления) узлов и перебалансирования данных между секциями. Этот подход может включать кластеризацию, в котором коллекция узлов представлена клиентским приложениям в виде простого, единого кэша. Однако данные распределяются между узлами после предопределенной стратегии распределения, которая равномерно распределяет нагрузку. Дополнительные сведения о возможных стратегиях секционирования см. в руководстве по секционированием данных.
Кластеризация также может увеличить доступность кэша. Если узел завершается сбоем, оставшаяся часть кэша по-прежнему доступна. Кластеризация часто используется в сочетании с репликацией и отработкой отказа. Каждый узел можно реплицировать, и реплика может быть быстро подключена к сети, если узел завершается ошибкой.
Многие операции чтения и записи, скорее всего, связаны с одними значениями данных или объектами. Однако иногда может потребоваться хранить или извлекать большие объемы данных. Например, создание кэша может включать запись сотен или тысяч элементов в кэш. Кроме того, приложению может потребоваться получить большое количество связанных элементов из кэша в рамках того же запроса.
Многие крупномасштабные кэши предоставляют пакетные операции для этих целей. Это позволяет клиентскому приложению упаковать большой объем элементов в один запрос и сократить затраты, связанные с выполнением большого количества небольших запросов.
Кэширование и итоговая согласованность
Для работы шаблона в стороне кэша экземпляр приложения, заполняющего кэш, должен иметь доступ к последней и согласованной версии данных. В системе, реализующей итоговую согласованность (например, реплицированное хранилище данных), это может быть не так.
Один экземпляр приложения может изменить элемент данных и недействительно использовать кэшированную версию этого элемента. Другой экземпляр приложения может попытаться прочитать этот элемент из кэша, что приводит к пропуску кэша, поэтому он считывает данные из хранилища данных и добавляет его в кэш. Однако если хранилище данных не было полностью синхронизировано с другими репликами, экземпляр приложения может считывать и заполнять кэш старым значением.
Дополнительные сведения об обработке согласованности данных см. в праймере согласованности данных.
Защита кэшированных данных
Независимо от используемой службы кэша рассмотрите возможность защиты данных, которые хранятся в кэше от несанкционированного доступа. Существует две основные проблемы:
- Конфиденциальность данных в кэше.
- Конфиденциальность данных по мере потоков между кэшем и приложением, использующим кэш.
Чтобы защитить данные в кэше, служба кэша может реализовать механизм проверки подлинности, который требует, чтобы приложения указали следующее:
- Какие удостоверения могут получить доступ к данным в кэше.
- Какие операции (чтение и запись) разрешены для выполнения этих удостоверений.
Чтобы уменьшить затраты, связанные с чтением и записью данных, после предоставления удостоверения записи или чтения к кэшу, это удостоверение может использовать любые данные в кэше.
Если необходимо ограничить доступ к подмножествам кэшированных данных, можно выполнить одно из следующих действий.
- Разделите кэш на секции (с помощью разных серверов кэша) и предоставьте доступ только удостоверениям для секций, которые они должны использовать.
- Зашифруйте данные в каждом подмножестве с помощью разных ключей и предоставьте ключи шифрования только удостоверениям, которые должны иметь доступ к каждому подмножество. Клиентское приложение по-прежнему может получить все данные в кэше, но только сможет расшифровать данные, для которых он имеет ключи.
Кроме того, необходимо защитить данные при его выходе из кэша. Для этого зависит от функций безопасности, предоставляемых сетевой инфраструктурой, используемой клиентскими приложениями для подключения к кэшу. Если кэш реализуется с помощью локального сервера в той же организации, где размещаются клиентские приложения, изоляция самой сети может не потребовать дополнительных действий. Если кэш находится удаленно и требует подключения TCP или HTTP через общедоступную сеть (например, Интернет), рассмотрите возможность реализации SSL.
Рекомендации по реализации кэширования в Azure
Кэш Azure для Redis — это реализация кэша Redis с открытым исходным кодом, который выполняется в качестве службы в центре обработки данных Azure. Она предоставляет службу кэширования, доступ к которому можно получить из любого приложения Azure, будь то приложение реализовано как облачная служба, веб-сайт или виртуальная машина Azure. Кэши могут совместно использоваться клиентскими приложениями с соответствующим ключом доступа.
Кэш Azure для Redis — это высокопроизводительное решение для кэширования, которое обеспечивает доступность, масштабируемость и безопасность. Обычно она выполняется как служба, распределенная по одному или нескольким выделенным компьютерам. Он пытается хранить столько информации, сколько она может в памяти, чтобы обеспечить быстрый доступ. Эта архитектура предназначена для обеспечения низкой задержки и высокой пропускной способности, уменьшая потребность в выполнении медленных операций ввода-вывода.
Кэш Azure для Redis совместим со многими различными API, которые используются клиентскими приложениями. Если у вас уже есть приложения, которые уже используют кэш Azure для Redis, работающих в локальной среде, кэш Azure для Redis предоставляет быстрый путь миграции к кэшированию в облаке.
Функции Redis
Redis — это больше, чем простой сервер кэша. Она предоставляет распределенную базу данных в памяти с обширным набором команд, поддерживающим множество распространенных сценариев. Они описаны далее в этом документе в разделе "Использование кэширования Redis". В этом разделе приведены некоторые ключевые функции, предоставляемые Redis.
Redis в качестве базы данных в памяти
Redis поддерживает операции чтения и записи. В Redis запись может быть защищена от сбоя системы путем периодического хранения в локальном файле моментального снимка или в файле журнала только для добавления. Эта ситуация не является делом во многих кэшах, которые следует рассматривать как временные хранилища данных.
Все записи являются асинхронными и не блокируют чтение и запись данных клиентами. При запуске Redis считывает данные из файла моментального снимка или журнала и использует его для создания кэша в памяти. Дополнительные сведения см. в статье о сохраняемости Redis на веб-сайте Redis.
Замечание
Redis не гарантирует, что все записи будут сохранены, если есть катастрофический сбой, но в худшем случае вы можете потерять только несколько секунд стоит данных. Помните, что кэш не предназначен для работы в качестве авторитетного источника данных, и это ответственность за приложения, использующие кэш, чтобы обеспечить успешное сохранение критически важных данных в соответствующем хранилище данных. Дополнительные сведения см. в шаблоне "Кэш в сторону".
Типы данных Redis
Redis — это хранилище значений ключей, где значения могут содержать простые типы или сложные структуры данных, такие как хэши, списки и наборы. Он поддерживает набор атомарных операций с этими типами данных. Ключи могут быть постоянными или помечены ограниченным временем жизни, в то время как ключ и соответствующее значение автоматически удаляются из кэша. Дополнительные сведения о ключах и значениях Redis см. на странице Введение в типы данных Redis и абстракции на веб-сайте Redis.
Репликация Redis и кластеризация
Redis поддерживает первичную или подчиненную репликацию для обеспечения доступности и поддержания пропускной способности. Операции записи в первичный узел Redis реплицируются на один или несколько подчиненных узлов. Операции чтения могут обслуживаться основным или любым из подчиненных.
Если у вас есть сетевая секция, подчиненные могут продолжать обслуживать данные, а затем прозрачно переназначать с основным при повторном развертывании подключения. Дополнительные сведения см. на странице репликации на веб-сайте Redis.
Redis также обеспечивает кластеризацию, что позволяет прозрачно секционировать данные на сегменты между серверами и распределять нагрузку. Эта функция повышает масштабируемость, так как новые серверы Redis можно добавлять и перераспределировать данные по мере увеличения размера кэша.
Кроме того, каждый сервер в кластере можно реплицировать с помощью первичной или подчиненной репликации. Это гарантирует доступность на каждом узле в кластере. Дополнительные сведения о кластеризации и сегментированиях см. на странице руководства по кластеру Redis на веб-сайте Redis.
Использование памяти Redis
Кэш Redis имеет ограниченный размер, который зависит от ресурсов, доступных на хост-компьютере. При настройке сервера Redis можно указать максимальный объем памяти, который он может использовать. Вы также можете настроить ключ в кэше Redis для истечения срока действия, после которого он автоматически удаляется из кэша. Эта функция может помочь предотвратить заполнение кэша в памяти старыми или устаревшими данными.
По мере заполнения памяти Redis может автоматически вытеснить ключи и их значения, выполнив ряд политик. По умолчанию используется LRU (по крайней мере используется), но вы также можете выбрать другие политики, такие как вытеснение ключей случайным образом или отключение вытеснения (в этом случае попытки добавления элементов в кэш завершаются ошибкой, если они полны). Страница "Использование Redis в качестве кэша LRU " предоставляет дополнительные сведения.
Транзакции и пакеты Redis
Redis позволяет клиентскому приложению отправлять ряд операций, которые считывают и записывают данные в кэше в виде атомарной транзакции. Все команды в транзакции гарантированно выполняются последовательно, и никакие команды, выданные другими параллельными клиентами, не будут перемежены между ними.
Однако эти транзакции не являются истинными, так как реляционная база данных будет выполнять их. Обработка транзакций состоит из двух этапов— первый — когда команды помещаются в очередь, а второй — при выполнении команд. На этапе очереди команд команды команды, составляющие транзакцию, отправляются клиентом. Если в данный момент возникает какая-то ошибка (например, синтаксическая ошибка или неправильное число параметров), Redis отказывается обработать всю транзакцию и отбрасывает ее.
На этапе выполнения Redis выполняет каждую команду в очереди в последовательности. Если команда завершается сбоем на этом этапе, Redis продолжает выполнять следующую команду в очереди и не откатывает эффекты уже запущенных команд. Эта упрощенная форма транзакций помогает поддерживать производительность и избегать проблем с производительностью, вызванными конфликтами.
Redis реализует форму оптимистической блокировки для обеспечения согласованности. Подробные сведения о транзакциях и блокировке с помощью Redis см. на странице "Транзакции " на веб-сайте Redis.
Redis также поддерживает нетрансляционную пакетную обработку запросов. Протокол Redis, используемый клиентами для отправки команд на сервер Redis, позволяет клиенту отправлять ряд операций в рамках одного запроса. Это может помочь уменьшить фрагментацию пакетов в сети. При обработке пакета каждая команда выполняется. Если какая-либо из этих команд неправильно сформирована, они будут отклонены (что не происходит с транзакцией), но остальные команды будут выполнены. Кроме того, нет никаких гарантий о порядке обработки команд в пакете.
Безопасность Redis
Redis ориентирован исключительно на обеспечение быстрого доступа к данным и предназначен для запуска в доверенной среде, к которой можно получить доступ только доверенным клиентам. Redis поддерживает ограниченную модель безопасности на основе проверки подлинности паролей. (Можно полностью удалить проверку подлинности, хотя мы не рекомендуем это.)
Все клиенты, прошедшие проверку подлинности, имеют одинаковый глобальный пароль и имеют доступ к тем же ресурсам. Если вам нужна более полная безопасность входа, необходимо реализовать собственный уровень безопасности перед сервером Redis, а все клиентские запросы должны проходить через этот дополнительный уровень. Redis не должен быть напрямую предоставлен ненадежным или неуверенным клиентам.
Вы можете ограничить доступ к командам, отключив их или переименовав их (и предоставив только привилегированные клиенты с новыми именами).
Redis не поддерживает ни одну форму шифрования данных, поэтому все кодировки должны выполняться клиентскими приложениями. Кроме того, Redis не предоставляет никакой формы безопасности транспорта. Если необходимо защитить данные по сети, рекомендуется реализовать SSL-прокси.
Дополнительные сведения см. на странице безопасности Redis на веб-сайте Redis.
Замечание
Кэш Azure для Redis предоставляет собственный уровень безопасности, через который клиенты подключаются. Базовые серверы Redis не предоставляются общедоступной сети.
Кэш Redis Для Azure
Кэш Azure для Redis предоставляет доступ к серверам Redis, размещенным в центре обработки данных Azure. Он выступает в качестве фасада, который обеспечивает контроль доступа и безопасность. Кэш можно подготовить с помощью портала Azure.
Портал предоставляет ряд предопределенных конфигураций. Они варьируются от кэша 53 ГБ, работающего в качестве выделенной службы, которая поддерживает ssl-связь (для конфиденциальности) и главного или подчиненного репликации с соглашением об уровне обслуживания (SLA) 99,9% доступности до 250 МБ кэша без репликации (без гарантий доступности), работающего на общем оборудовании.
С помощью портала Azure можно также настроить политику вытеснения кэша и управлять доступом к кэшу, добавив пользователей в предоставленные роли. Эти роли, определяющие операции, которые могут выполнять члены, включают владельца, участника и читателя. Например, члены роли владельца имеют полный контроль над кэшем (включая безопасность) и его содержимое, члены роли участника могут считывать и записывать сведения в кэше, а члены роли чтения могут извлекать данные только из кэша.
Большинство административных задач выполняются на портале Azure. По этой причине многие административные команды, доступные в стандартной версии Redis, недоступны, включая возможность программного изменения конфигурации, завершение работы сервера Redis, настройка дополнительных подчиненных или принудительное сохранение данных на диск.
Портал Azure включает удобный графический дисплей, позволяющий отслеживать производительность кэша. Например, можно просмотреть количество выполненных подключений, количество выполняемых запросов, объем операций чтения и записи, а также количество попаданий кэша и пропущенных кэшей. Используя эти сведения, можно определить эффективность кэша и при необходимости переключиться на другую конфигурацию или изменить политику вытеснения.
Кроме того, вы можете создавать оповещения, отправляющие сообщения электронной почты администратору, если одна или несколько критически важных метрик выходят за пределы ожидаемого диапазона. Например, может потребоваться предупредить администратора, если количество пропущенных кэшей превышает указанное значение за последний час, так как это означает, что кэш может быть слишком мал или данные могут быть вытеснимы слишком быстро.
Вы также можете отслеживать использование ЦП, памяти и сети для кэша.
Дополнительные сведения и примеры, показывающие, как создать и настроить кэш Azure для Redis, перейдите на страницу Lap вокруг кэша Azure для Redis в блоге Azure.
Кэширование состояния сеанса и выходных данных HTML
Если вы создаете ASP.NET веб-приложения, которые выполняются с помощью веб-ролей Azure, вы можете сохранить сведения о состоянии сеанса и выходные данные HTML в кэше Azure для Redis. Поставщик состояний сеанса для Кэша Azure для Redis позволяет совместно использовать сведения о сеансах между различными экземплярами веб-приложения ASP.NET и очень полезно в ситуациях веб-фермы, когда сходство между клиентским сервером недоступно и кэширование данных сеанса в памяти не будет подходящим.
Использование поставщика состояний сеанса с кэшем Azure для Redis обеспечивает несколько преимуществ, в том числе:
- Совместное использование состояния сеанса с большим количеством экземпляров веб-приложений ASP.NET.
- Обеспечение улучшенной масштабируемости.
- Поддержка управляемого, параллельного доступа к одному и тому же состоянию сеанса для нескольких читателей и одного средства записи.
- Использование сжатия для сохранения памяти и повышения производительности сети.
Дополнительные сведения см. в разделе ASP.NET поставщик состояния сеанса для кэша Azure для Redis.
Замечание
Не используйте поставщик состояний сеанса для кэша Azure для Redis с ASP.NET приложениями, работающими за пределами среды Azure. Задержка доступа к кэшу из-за пределов Azure может устранить преимущества кэширования данных.
Аналогичным образом поставщик кэша выходных данных для Кэша Azure для Redis позволяет сохранять HTTP-ответы, созданные веб-приложением ASP.NET. Использование поставщика кэша выходных данных с кэшем Azure для Redis может улучшить время отклика приложений, которые отображают сложные выходные данные HTML. Экземпляры приложений, которые создают аналогичные ответы, могут использовать общие выходные фрагменты в кэше, а не создавать эти выходные данные HTML. Дополнительные сведения см. в разделе ASP.NET поставщика кэша выходных данных для Кэша Azure для Redis.
Создание пользовательского кэша Redis
Кэш Azure для Redis выступает в качестве фасада для базовых серверов Redis. Если требуется расширенная конфигурация, которая не охватывается кэшем Redis Azure (например, кэш размером более 53 ГБ), можно создавать и размещать собственные серверы Redis с помощью виртуальных машин Azure.
Это потенциально сложный процесс, так как может потребоваться создать несколько виртуальных машин для работы в качестве первичных и подчиненных узлов, если требуется реализовать репликацию. Кроме того, если вы хотите создать кластер, вам потребуется несколько первичных и подчиненных серверов. Минимальная топология кластеризованной репликации, которая обеспечивает высокую степень доступности и масштабируемости, включает по крайней мере шесть виртуальных машин, организованных как три пары первичных и подчиненных серверов (кластер должен содержать не менее трех первичных узлов).
Каждая первичная или подчиненная пара должна находиться близко друг к другу, чтобы свести к минимуму задержку. Однако каждый набор пар может выполняться в разных центрах обработки данных Azure, расположенных в разных регионах, если вы хотите найти кэшированные данные близко к приложениям, которые, скорее всего, будут использовать его. Пример создания и настройки узла Redis, работающего как виртуальная машина Azure, см. в статье "Запуск Redis" на виртуальной машине Linux CentOS в Azure.
Замечание
Если вы реализуете собственный кэш Redis таким образом, вы несете ответственность за мониторинг, управление и защиту службы.
Секционирование кэша Redis
Секционирование кэша включает разделение кэша на нескольких компьютерах. Эта структура дает несколько преимуществ по сравнению с одним сервером кэша, включая:
- Создание кэша, который гораздо больше, чем может храниться на одном сервере.
- Распределение данных между серверами, повышение доступности. Если один сервер завершается сбоем или становится недоступным, данные, которые он содержит, недоступны, но доступ к данным на оставшихся серверах по-прежнему можно получить. Для кэша это не важно, так как кэшированные данные — это только временная копия данных, которые хранятся в базе данных. Кэшированные данные на сервере, который становится недоступным, можно кэшировать на другом сервере.
- Распределение нагрузки между серверами, тем самым повышая производительность и масштабируемость.
- Геолокация данных близко к пользователям, к которым он обращается, что снижает задержку.
Для кэша наиболее распространенная форма секционирования — сегментирование. В этой стратегии каждый раздел (или сегмент) — это кэш Redis в собственном праве. Данные направляются в определенную секцию с помощью логики сегментирования, которая может использовать различные подходы для распределения данных. Шаблон сегментирования предоставляет дополнительные сведения о реализации сегментирования.
Чтобы реализовать секционирование в кэше Redis, можно использовать один из следующих подходов:
- Маршрутизация запросов на стороне сервера. В этом методе клиентское приложение отправляет запрос на любой из серверов Redis, составляющих кэш (вероятно, ближайший сервер). Каждый сервер Redis хранит метаданные, описывающие удерживаемую секцию, а также содержит сведения о том, какие секции находятся на других серверах. Сервер Redis проверяет запрос клиента. Если ее можно разрешить локально, она выполнит запрошенную операцию. В противном случае он перенаправит запрос на соответствующий сервер. Эта модель реализована кластеризированием Redis и подробно описана на странице руководства по кластеру Redis на веб-сайте Redis. Кластеризация Redis является прозрачной для клиентских приложений, а дополнительные серверы Redis можно добавить в кластер (и повторное развертывание данных), не требуя перенастройки клиентов.
- Секционирование на стороне клиента. В этой модели клиентское приложение содержит логику (возможно, в виде библиотеки), которая направляет запросы на соответствующий сервер Redis. Этот подход можно использовать с кэшем Azure для Redis. Создайте несколько кэша Azure для Redis (по одному для каждой секции данных) и реализуйте клиентская логика, которая направляет запросы в правильный кэш. Если схема секционирования изменяется (если создается дополнительный кэш Azure для Redis, например), клиентские приложения могут быть перенастроены.
- Секционирование с помощью прокси-сервера. В этой схеме клиентские приложения отправляют запросы в промежуточную прокси-службу, которая понимает, как данные секционируются, а затем направляет запрос на соответствующий сервер Redis. Этот подход также можно использовать с кэшем Azure для Redis; Прокси-служба может быть реализована как облачная служба Azure. Этот подход требует дополнительного уровня сложности для реализации службы, и запросы могут занять больше времени, чем использование секционирования на стороне клиента.
Секционирование страниц: разделение данных между несколькими экземплярами Redis на веб-сайте Redis предоставляет дополнительные сведения о реализации секционирования с помощью Redis.
Реализация клиентских приложений кэша Redis
Redis поддерживает клиентские приложения, написанные на многочисленных языках программирования. При создании новых приложений с помощью .NET Framework рекомендуется использовать клиентская библиотека StackExchange.Redis. Эта библиотека предоставляет объектную модель .NET Framework, которая абстрагирует сведения о подключении к серверу Redis, отправке команд и получении ответов. Он доступен в Visual Studio в виде пакета NuGet. Эту же библиотеку можно использовать для подключения к кэшу Azure для Redis или пользовательскому кэшу Redis, размещенному на виртуальной машине.
Для подключения к серверу Redis используется статический Connect
метод ConnectionMultiplexer
класса. Созданное этим методом соединение предназначено для использования в течение всего времени существования клиентского приложения, а одно и то же подключение может использоваться несколькими параллельными потоками. Не подключайтесь и не отключайтесь при каждом выполнении операции Redis, так как это может снизить производительность.
Можно указать параметры подключения, такие как адрес узла Redis и пароль. При использовании кэша Azure для Redis пароль является первичным или вторичным ключом, созданным для кэша Azure для Redis с помощью портала Azure.
После подключения к серверу Redis можно получить дескриптор в базе данных Redis, которая выступает в качестве кэша. Подключение Redis предоставляет GetDatabase
метод для этого. Затем можно извлечь элементы из кэша и сохранить данные в кэше с помощью StringGet
методов и StringSet
методов. Эти методы ожидают ключ в качестве параметра и возвращают элемент в кэше с соответствующим значением (StringGet
) или добавьте элемент в кэш с этим ключом (StringSet
).
В зависимости от расположения сервера Redis многие операции могут привести к некоторой задержке во время передачи запроса на сервер, а ответ возвращается клиенту. Библиотека StackExchange предоставляет асинхронные версии многих методов, предоставляемых им, чтобы помочь клиентским приложениям оставаться адаптивными. Эти методы поддерживают асинхронный шаблон на основе задач в .NET Framework.
В следующем фрагменте кода показан метод с именем RetrieveItem
. Он иллюстрирует реализацию шаблона в стороне кэша на основе Redis и библиотеки StackExchange. Метод принимает строковое значение ключа и пытается получить соответствующий элемент из кэша Redis путем вызова StringGetAsync
метода (асинхронная версия StringGet
).
Если элемент не найден, он извлекается из базового источника данных с помощью GetItemFromDataSourceAsync
метода (который является локальным методом, а не частью библиотеки StackExchange). Затем он добавляется в кэш с помощью StringSetAsync
метода, чтобы его можно было получить быстрее при следующем запуске.
// Connect to the Azure Redis cache
ConfigurationOptions config = new ConfigurationOptions();
config.EndPoints.Add("<your DNS name>.redis.cache.windows.net");
config.Password = "<Redis cache key from management portal>";
ConnectionMultiplexer redisHostConnection = ConnectionMultiplexer.Connect(config);
IDatabase cache = redisHostConnection.GetDatabase();
...
private async Task<string> RetrieveItem(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 == null)
{
itemValue = await GetItemFromDataSourceAsync(itemKey);
await cache.StringSetAsync(itemKey, itemValue);
}
// Return the item
return itemValue;
}
StringSet
Методы StringGet
не ограничиваются извлечением или хранением строковых значений. Они могут принимать любой элемент, сериализованный в виде массива байтов. Если необходимо сохранить объект .NET, его можно сериализовать в виде потока байтов и использовать StringSet
метод для записи в кэш.
Аналогичным образом можно считывать объект из кэша StringGet
с помощью метода и десериализации его в виде объекта .NET. В следующем коде показан набор методов расширения для интерфейса IDatabase ( GetDatabase
метод подключения Redis возвращает IDatabase
объект), а также некоторый пример кода, который использует эти методы для чтения и записи BlogPost
объекта в кэш:
public static class RedisCacheExtensions
{
public static async Task<T> GetAsync<T>(this IDatabase cache, string key)
{
return Deserialize<T>(await cache.StringGetAsync(key));
}
public static async Task<object> GetAsync(this IDatabase cache, string key)
{
return Deserialize<object>(await cache.StringGetAsync(key));
}
public static async Task SetAsync(this IDatabase cache, string key, object value)
{
await cache.StringSetAsync(key, Serialize(value));
}
static byte[] Serialize(object o)
{
byte[] objectDataAsStream = null;
if (o != null)
{
var jsonString = JsonSerializer.Serialize(o);
objectDataAsStream = Encoding.ASCII.GetBytes(jsonString);
}
return objectDataAsStream;
}
static T Deserialize<T>(byte[] stream)
{
T result = default(T);
if (stream != null)
{
var jsonString = Encoding.ASCII.GetString(stream);
result = JsonSerializer.Deserialize<T>(jsonString);
}
return result;
}
}
Следующий код иллюстрирует метод с именем RetrieveBlogPost
, использующий эти методы расширения для чтения и записи сериализуемого BlogPost
объекта в кэш после шаблона в сторону кэша:
// The BlogPost type
public class BlogPost
{
private HashSet<string> tags;
public BlogPost(int id, string title, int score, IEnumerable<string> tags)
{
this.Id = id;
this.Title = title;
this.Score = score;
this.tags = new HashSet<string>(tags);
}
public int Id { get; set; }
public string Title { get; set; }
public int Score { get; set; }
public ICollection<string> Tags => this.tags;
}
...
private async Task<BlogPost> RetrieveBlogPost(string blogPostKey)
{
BlogPost blogPost = await cache.GetAsync<BlogPost>(blogPostKey);
if (blogPost == null)
{
blogPost = await GetBlogPostFromDataSourceAsync(blogPostKey);
await cache.SetAsync(blogPostKey, blogPost);
}
return blogPost;
}
Redis поддерживает конвейер команды, если клиентское приложение отправляет несколько асинхронных запросов. Redis может мультиплексировать запросы с помощью одного подключения, а не получать и отвечать на команды в строгой последовательности.
Этот подход помогает сократить задержку, делая более эффективным использование сети. В следующем фрагменте кода показан пример, который извлекает сведения о двух клиентах одновременно. Код отправляет два запроса, а затем выполняет другую обработку (не отображается), прежде чем ожидать получения результатов. Метод Wait
объекта кэша аналогичен методу .NET Framework Task.Wait
:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
var task1 = cache.StringGetAsync("customer:1");
var task2 = cache.StringGetAsync("customer:2");
...
var customer1 = cache.Wait(task1);
var customer2 = cache.Wait(task2);
Дополнительные сведения о написании клиентских приложений, которые могут использовать кэш Azure для Redis, см. в документации по кэшу Azure для Redis. Дополнительные сведения также доступны в StackExchange.Redis.
На одном веб-сайте конвейеры и мультиплексоры страницы содержатся дополнительные сведения об асинхронных операциях и конвейерах с помощью Redis и библиотеки StackExchange.
Использование кэширования Redis
Самое простое использование Redis для проблем кэширования — это пары "ключ-значение", где значение является непреднамереной строкой произвольной длины, которая может содержать любые двоичные данные. (Это, по сути, массив байтов, которые можно рассматривать как строку). Этот сценарий показан в разделе "Реализация клиентских приложений кэша Redis" ранее в этой статье.
Обратите внимание, что ключи также содержат непреднамеренные данные, поэтому в качестве ключа можно использовать любую двоичную информацию. Чем дольше ключ, тем больше места потребуется для хранения, и тем больше времени потребуется для выполнения операций подстановки. Для удобства использования и удобства обслуживания необходимо тщательно разработать пространство ключей и использовать значимые (но не подробные) ключи.
Например, используйте структурированные ключи, такие как customer:100, чтобы представить ключ для клиента с идентификатором 100, а не просто "100". Эта схема позволяет легко различать значения, которые хранят разные типы данных. Например, можно также использовать ключ "orders:100", чтобы представить ключ для заказа с идентификатором 100.
Помимо одномерных двоичных строк, значение в паре "Ключ-значение Redis" также может содержать более структурированные сведения, включая списки, наборы (отсортированные и несортированные) и хэши. Redis предоставляет полный набор команд, который может управлять этими типами, и многие из этих команд доступны для приложений .NET Framework через клиентская библиотека, например StackExchange. Страница Введение в типы данных Redis и абстракции на веб-сайте Redis содержит более подробный обзор этих типов и команд, которые можно использовать для управления ими.
В этом разделе приведены некоторые распространенные варианты использования этих типов данных и команд.
Выполнение атомарных и пакетных операций
Redis поддерживает ряд атомарных операций получения и задания строковых значений. Эти операции удаляют возможные риски расы, которые могут возникать при использовании отдельных GET
команд и SET
команд. Доступные операции:
INCR
, ,INCRBY
DECR
иDECRBY
, которые выполняют атомарные инкрементные операции с целыми числовыми значениями данных. Библиотека StackExchange предоставляет перегруженные версии иIDatabase.StringDecrementAsync
методыIDatabase.StringIncrementAsync
для выполнения этих операций и возврата результирующего значения, хранящегося в кэше. В следующем фрагменте кода показано, как использовать следующие методы:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... await cache.StringSetAsync("data:counter", 99); ... long oldValue = await cache.StringIncrementAsync("data:counter"); // Increment by 1 (the default) // oldValue should be 100 long newValue = await cache.StringDecrementAsync("data:counter", 50); // Decrement by 50 // newValue should be 50
GETSET
, который получает значение, связанное с ключом, и изменяет его на новое значение. Библиотека StackExchange делает эту операцию доступнойIDatabase.StringGetSetAsync
с помощью метода. В приведенном ниже фрагменте кода показан пример этого метода. Этот код возвращает текущее значение, связанное с ключом data:counter из предыдущего примера. Затем он сбрасывает значение для этого ключа обратно к нулю, все в рамках одной операции:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... string oldValue = await cache.StringGetSetAsync("data:counter", 0);
MGET
иMSET
, которое может возвращать или изменять набор строковых значений в виде одной операции.IDatabase.StringSetAsync
МетодыIDatabase.StringGetAsync
перегружены для поддержки этой функции, как показано в следующем примере:ConnectionMultiplexer redisHostConnection = ...; IDatabase cache = redisHostConnection.GetDatabase(); ... // Create a list of key-value pairs var keysAndValues = new List<KeyValuePair<RedisKey, RedisValue>>() { new KeyValuePair<RedisKey, RedisValue>("data:key1", "value1"), new KeyValuePair<RedisKey, RedisValue>("data:key99", "value2"), new KeyValuePair<RedisKey, RedisValue>("data:key322", "value3") }; // Store the list of key-value pairs in the cache cache.StringSet(keysAndValues.ToArray()); ... // 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 = cache.StringGet(keys);
Вы также можете объединить несколько операций в одну транзакцию Redis, как описано в разделе транзакций Redis и пакетных пакетов, описанных ранее в этой статье. Библиотека StackExchange обеспечивает поддержку транзакций ITransaction
через интерфейс.
Объект создается ITransaction
с помощью IDatabase.CreateTransaction
метода. Команды для транзакции вызываются с помощью методов, предоставляемых ITransaction
объектом.
Интерфейс ITransaction
предоставляет доступ к набору методов, которые похожи на те, к которым обращается IDatabase
интерфейс, за исключением того, что все методы являются асинхронными. Это означает, что они выполняются только при вызове ITransaction.Execute
метода. Значение, возвращаемое методом ITransaction.Execute
, указывает, успешно ли была создана транзакция (true) или если она завершилась ошибкой (false).
В следующем фрагменте кода показан пример, который увеличивает и уменьшает два счетчика в рамках одной транзакции:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
ITransaction transaction = cache.CreateTransaction();
var tx1 = transaction.StringIncrementAsync("data:counter1");
var tx2 = transaction.StringDecrementAsync("data:counter2");
bool result = transaction.Execute();
Console.WriteLine("Transaction {0}", result ? "succeeded" : "failed");
Console.WriteLine("Result of increment: {0}", tx1.Result);
Console.WriteLine("Result of decrement: {0}", tx2.Result);
Помните, что транзакции Redis отличаются от транзакций в реляционных базах данных. Метод Execute
просто помещает в очередь все команды, составляющие транзакцию для выполнения, и если какая-либо из них неправильно сформирована, транзакция остановлена. Если все команды были успешно поставлены в очередь, каждая команда выполняется асинхронно.
Если любая команда завершается ошибкой, остальные по-прежнему продолжают обработку. Если необходимо убедиться, что команда выполнена успешно, необходимо получить результаты команды с помощью свойства Result соответствующей задачи, как показано в приведенном выше примере. Чтение свойства Result блокирует вызывающий поток до завершения задачи.
Дополнительные сведения см. в разделе "Транзакции" в Redis.
При выполнении пакетных операций можно использовать IBatch
интерфейс библиотеки StackExchange. Этот интерфейс предоставляет доступ к набору методов, аналогичных тем, к которым обращается IDatabase
интерфейс, за исключением того, что все методы являются асинхронными.
Объект создается IBatch
с помощью IDatabase.CreateBatch
метода, а затем выполняется пакет с помощью IBatch.Execute
метода, как показано в следующем примере. Этот код просто задает строковое значение, увеличивает и уменьшает те же счетчики, используемые в предыдущем примере, и отображает результаты:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
IBatch batch = cache.CreateBatch();
batch.StringSetAsync("data:key1", 11);
var t1 = batch.StringIncrementAsync("data:counter1");
var t2 = batch.StringDecrementAsync("data:counter2");
batch.Execute();
Console.WriteLine("{0}", t1.Result);
Console.WriteLine("{0}", t2.Result);
Важно понимать, что в отличие от транзакции, если команда в пакете завершается ошибкой, так как она неправильно сформирована, другие команды по-прежнему могут выполняться. Метод IBatch.Execute
не возвращает никаких признаков успешности или сбоя.
Выполнение операций с огнем и забыли об операциях кэша
Redis поддерживает операции с огнем и забыли с помощью флагов команд. В этой ситуации клиент просто инициирует операцию, но не имеет интереса к результату и не ожидает завершения команды. В приведенном ниже примере показано, как выполнить команду INCR в качестве операции пожара и забыть:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
await cache.StringSetAsync("data:key1", 99);
...
cache.StringIncrement("data:key1", flags: CommandFlags.FireAndForget);
Укажите автоматически истекающий срок действия ключей
При хранении элемента в кэше Redis можно указать время ожидания, после которого элемент будет автоматически удален из кэша. Вы также можете запросить, сколько времени у ключа до истечения срока его действия с помощью TTL
команды. Эта команда доступна для приложений StackExchange с помощью IDatabase.KeyTimeToLive
метода.
В следующем фрагменте кода показано, как задать время окончания срока действия ключа в течение 20 секунд и запросить оставшееся время существования ключа:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// 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 a null
TimeSpan? expiry = cache.KeyTimeToLive("data:key1");
Вы также можете задать время окончания срока действия для определенной даты и времени с помощью команды EXPIRE, которая доступна в библиотеке StackExchange в качестве KeyExpireAsync
метода:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Add a key with an expiration date of midnight on 1st January 2015
await cache.StringSetAsync("data:key1", 99);
await cache.KeyExpireAsync("data:key1",
new DateTime(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc));
...
Подсказка
Вы можете вручную удалить элемент из кэша с помощью команды DEL, которая доступна через библиотеку StackExchange в качестве IDatabase.KeyDeleteAsync
метода.
Использование тегов для перекрестной корреляции кэшированных элементов
Набор Redis — это коллекция нескольких элементов, которые совместно используют один ключ. Вы можете создать набор с помощью команды SADD. Элементы в наборе можно получить с помощью команды SMEMBERS. Библиотека StackExchange реализует команду SADD с IDatabase.SetAddAsync
методом и команду SMEMBERS с методом IDatabase.SetMembersAsync
.
Вы также можете объединить существующие наборы для создания новых наборов с помощью команд SDIFF (set difference), SINTER (set intersection) и SUNION (set union). Библиотека StackExchange объединяет эти операции в методе IDatabase.SetCombineAsync
. Первый параметр этого метода указывает операцию набора, выполняемую.
В следующих фрагментах кода показано, как наборы могут быть полезны для быстрого хранения и получения коллекций связанных элементов. Этот код использует тип, описанный BlogPost
в разделе "Реализация клиентских приложений кэша Redis" ранее в этой статье.
Объект BlogPost
содержит четыре поля: идентификатор, название, оценка ранжирования и коллекция тегов. В первом фрагменте кода ниже показаны примеры данных, которые используются для заполнения списка BlogPost
объектов C#:
List<string[]> tags = new List<string[]>
{
new[] { "iot","csharp" },
new[] { "iot","azure","csharp" },
new[] { "csharp","git","big data" },
new[] { "iot","git","database" },
new[] { "database","git" },
new[] { "csharp","database" },
new[] { "iot" },
new[] { "iot","database","git" },
new[] { "azure","database","big data","git","csharp" },
new[] { "azure" }
};
List<BlogPost> posts = new List<BlogPost>();
int blogKey = 0;
int numberOfPosts = 20;
Random random = new Random();
for (int i = 0; i < numberOfPosts; i++)
{
blogKey++;
posts.Add(new BlogPost(
blogKey, // Blog post ID
string.Format(CultureInfo.InvariantCulture, "Blog Post #{0}",
blogKey), // Blog post title
random.Next(100, 10000), // Ranking score
tags[i % tags.Count])); // Tags--assigned from a collection
// in the tags list
}
Теги для каждого BlogPost
объекта можно сохранить в кэше Redis и связать каждый набор с идентификатором BlogPost
объекта. Это позволяет приложению быстро найти все теги, принадлежащие определенной записи блога. Чтобы включить поиск в противоположном направлении и найти все записи блога, которые совместно используют определенный тег, можно создать другой набор, содержащий записи блога, ссылающиеся на идентификатор тега в ключе:
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
// Tags are easily represented as Redis Sets
foreach (BlogPost post in posts)
{
string redisKey = string.Format(CultureInfo.InvariantCulture,
"blog:posts:{0}:tags", post.Id);
// Add tags to the blog post in Redis
await cache.SetAddAsync(
redisKey, post.Tags.Select(s => (RedisValue)s).ToArray());
// Now do the inverse so we can figure out which blog posts have a given tag
foreach (var tag in post.Tags)
{
await cache.SetAddAsync(string.Format(CultureInfo.InvariantCulture,
"tag:{0}:blog:posts", tag), post.Id);
}
}
Эти структуры позволяют эффективно выполнять множество распространенных запросов. Например, можно найти и отобразить все теги для записи блога 1 следующим образом:
// Show the tags for blog post #1
foreach (var value in await cache.SetMembersAsync("blog:posts:1:tags"))
{
Console.WriteLine(value);
}
Вы можете найти все теги, которые являются общими для записи блога 1 и записи блога 2, выполнив заданную операцию пересечения, как показано ниже.
// Show the tags in common for blog posts #1 and #2
foreach (var value in await cache.SetCombineAsync(SetOperation.Intersect, new RedisKey[]
{ "blog:posts:1:tags", "blog:posts:2:tags" }))
{
Console.WriteLine(value);
}
И вы можете найти все записи блога, содержащие определенный тег:
// Show the ids of the blog posts that have the tag "iot".
foreach (var value in await cache.SetMembersAsync("tag:iot:blog:posts"))
{
Console.WriteLine(value);
}
Поиск недавно доступных элементов
Обычной задачей, необходимой для многих приложений, является поиск последних доступных элементов. Например, сайт блога может потребоваться отобразить сведения о последних записях блога.
Эту функцию можно реализовать с помощью списка Redis. Список Redis содержит несколько элементов, которые используют один и тот же ключ. Список выступает в качестве двойной очереди. Вы можете отправлять элементы в любой конец списка с помощью команд LPUSH (слева) и RPUSH (справа). Элементы из любого конца списка можно получить с помощью команд LPOP и RPOP. Вы также можете вернуть набор элементов с помощью команд LRANGE и RRANGE.
В приведенных ниже фрагментах кода показано, как выполнять эти операции с помощью библиотеки StackExchange. Этот код использует BlogPost
тип из предыдущих примеров. Как запись блога читается пользователем, IDatabase.ListLeftPushAsync
метод отправляет название записи блога в список, связанный с ключом "блог:recent_posts" в кэше Redis.
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:recent_posts";
BlogPost blogPost = ...; // Reference to the blog post that has just been read
await cache.ListLeftPushAsync(
redisKey, blogPost.Title); // Push the blog post onto the list
По мере того как больше записей блога читаются, их заголовки толкаются в тот же список. Список упорядочен последовательностью, в которой были добавлены заголовки. Последние записи блога слева от списка. (Если одна запись блога читается несколько раз, она будет содержать несколько записей в списке.)
Заголовки последних записей можно отобразить с помощью IDatabase.ListRange
метода. Этот метод принимает ключ, содержащий список, начальную точку и конечную точку. Следующий код извлекает заголовки записей блога 10 (элементы от 0 до 9) в левом конце списка:
// Show latest ten posts
foreach (string postTitle in await cache.ListRangeAsync(redisKey, 0, 9))
{
Console.WriteLine(postTitle);
}
Обратите внимание, что ListRangeAsync
метод не удаляет элементы из списка. Для этого можно использовать IDatabase.ListLeftPopAsync
методы и IDatabase.ListRightPopAsync
методы.
Чтобы предотвратить рост списка на неопределенный срок, вы можете периодически обрезать элементы, обрезав список. В приведенном ниже фрагменте кода показано, как удалить все элементы, кроме пяти левых элементов из списка:
await cache.ListTrimAsync(redisKey, 0, 5);
Реализация таблицы лидеров
По умолчанию элементы в наборе не хранятся в определенном порядке. Вы можете создать упорядоченный набор с помощью команды ZADD ( IDatabase.SortedSetAdd
метод в библиотеке StackExchange). Элементы упорядочены с помощью числового значения, называемого оценкой, которая предоставляется в качестве параметра для команды.
Следующий фрагмент кода добавляет заголовок записи блога в упорядоченный список. В этом примере каждая запись блога также содержит поле оценки, содержащее ранжирование записи блога.
ConnectionMultiplexer redisHostConnection = ...;
IDatabase cache = redisHostConnection.GetDatabase();
...
string redisKey = "blog:post_rankings";
BlogPost blogPost = ...; // Reference to a blog post that has just been rated
await cache.SortedSetAddAsync(redisKey, blogPost.Title, blogPost.Score);
Вы можете получить заголовки и оценки блога в порядке оценки по возрастанию с помощью IDatabase.SortedSetRangeByRankWithScores
метода:
foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(redisKey))
{
Console.WriteLine(post);
}
Замечание
Библиотека StackExchange также предоставляет IDatabase.SortedSetRangeByRankAsync
метод, который возвращает данные в порядке оценки, но не возвращает оценки.
Вы также можете получить элементы в порядке убывания показателей и ограничить количество возвращаемых элементов, предоставив дополнительные параметры методу IDatabase.SortedSetRangeByRankWithScoresAsync
. В следующем примере отображаются заголовки и оценки лучших 10 рейтинговых записей блога:
foreach (var post in await cache.SortedSetRangeByRankWithScoresAsync(
redisKey, 0, 9, Order.Descending))
{
Console.WriteLine(post);
}
В следующем примере используется IDatabase.SortedSetRangeByScoreWithScoresAsync
метод, который можно использовать для ограничения элементов, возвращаемых тем, которые попадают в заданный диапазон показателей:
// Blog posts with scores between 5000 and 100000
foreach (var post in await cache.SortedSetRangeByScoreWithScoresAsync(
redisKey, 5000, 100000))
{
Console.WriteLine(post);
}
Сообщение с помощью каналов
Помимо использования кэша данных, сервер Redis предоставляет обмен сообщениями с помощью механизма высокопроизводительного издателя или подписчика. Клиентские приложения могут подписаться на канал, а другие приложения или службы могут публиковать сообщения в канале. Подписывание приложений будет получать эти сообщения и обрабатывать их.
Redis предоставляет команду SUBSCRIBE для клиентских приложений, которые используются для подписки на каналы. Эта команда ожидает имя одного или нескольких каналов, на которых приложение будет принимать сообщения. Библиотека StackExchange включает ISubscription
интерфейс, который позволяет приложению .NET Framework подписаться и опубликовать их в каналах.
Объект создается ISubscription
с помощью GetSubscriber
метода подключения к серверу Redis. Затем вы прослушиваете сообщения в канале SubscribeAsync
с помощью метода этого объекта. В следующем примере кода показано, как подписаться на канал с именем messages:blogPosts:
ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
await subscriber.SubscribeAsync("messages:blogPosts", (channel, message) => Console.WriteLine("Title is: {0}", message));
Первым параметром метода Subscribe
является имя канала. Это имя следует тем же соглашениям, которые используются ключами в кэше. Имя может содержать любые двоичные данные, но мы рекомендуем использовать относительно короткие, значимые строки, чтобы обеспечить хорошую производительность и удобство обслуживания.
Обратите внимание, что пространство имен, используемое каналами, отличается от пространства имен, используемого ключами. Это означает, что у вас могут быть каналы и ключи с одинаковым именем, хотя это может сделать код приложения более сложным для обслуживания.
Второй параметр — делегат Action. Этот делегат выполняется асинхронно при появлении нового сообщения на канале. В этом примере просто отображается сообщение на консоли (сообщение будет содержать название записи блога).
Для публикации в канале приложение может использовать команду Redis PUBLISH. Библиотека StackExchange предоставляет IServer.PublishAsync
метод для выполнения этой операции. В следующем фрагменте кода показано, как опубликовать сообщение в канале message:blogPosts:
ConnectionMultiplexer redisHostConnection = ...;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
...
BlogPost blogPost = ...;
subscriber.PublishAsync("messages:blogPosts", blogPost.Title);
Существует несколько точек, которые необходимо понять о механизме публикации и подписки:
- Несколько подписчиков могут подписаться на один канал, и все они получат сообщения, опубликованные в этом канале.
- Подписчики получают только сообщения, опубликованные после подписки. Каналы не буферичены, и после публикации сообщения инфраструктура Redis отправляет сообщение каждому подписчику, а затем удаляет его.
- По умолчанию сообщения получаются подписчиками в том порядке, в котором они отправляются. В высокоактивной системе с большим количеством сообщений и многими подписчиками и издателями гарантированная последовательная доставка сообщений может замедлить производительность системы. Если каждое сообщение является независимым и порядок не имеет значение, можно включить параллельную обработку системой Redis, что может помочь повысить скорость реагирования. Это можно достичь в клиенте StackExchange, задав параметр PreserveAsyncOrder подключения, используемого подписчиком, значение false:
ConnectionMultiplexer redisHostConnection = ...;
redisHostConnection.PreserveAsyncOrder = false;
ISubscriber subscriber = redisHostConnection.GetSubscriber();
Рекомендации по сериализации
При выборе формата сериализации рассмотрите компромиссы между производительностью, взаимодействием, версиями, совместимостью с существующими системами, сжатием данных и затратами на память. При оценке производительности помните, что тесты зависят от контекста. Они могут не отражать фактическую рабочую нагрузку и могут не рассматривать более новые библиотеки или версии. Для всех сценариев нет единого сериализатора "быстрый".
Некоторые варианты, которые следует рассмотреть, включают в себя:
Буферы протокола (также называемые protobuf) — это формат сериализации, разработанный Google для эффективной сериализации структурированных данных. Он использует строго типизированные файлы определений для определения структур сообщений. Затем эти файлы определения компилируются в код для конкретного языка для сериализации и десериализации сообщений. Protobuf можно использовать для существующих механизмов RPC или создать службу RPC.
Apache Thrift использует аналогичный подход с строго типизированными файлами определения и этапом компиляции для создания кода сериализации и служб RPC.
Apache Avro предоставляет аналогичные функции буферов протокола и Thrift, но нет шага компиляции. Вместо этого сериализованные данные всегда содержат схему, описывающую структуру.
JSON — это открытый стандарт, использующий текстовые поля, доступные для чтения пользователем. Она имеет широкую кроссплатформенную поддержку. JSON не использует схемы сообщений. Будучи текстовым форматом, это не очень эффективно по проводу. Однако в некоторых случаях вы можете вернуть кэшированные элементы непосредственно клиенту через HTTP, в этом случае сохранение JSON может сэкономить затраты на десериализацию из другого формата, а затем сериализовать в JSON.
binary JSON (BSON) — это формат двоичной сериализации, который использует структуру, аналогичную JSON. BSON был разработан для упрощения, простого сканирования и быстрого сериализации и десериализации относительно JSON. Полезные данные сравнимы по размеру с JSON. В зависимости от данных полезные данные BSON могут быть меньше или больше полезных данных JSON. BSON содержит некоторые дополнительные типы данных, которые недоступны в ФОРМАТЕ JSON, в частности BinData (для массивов байтов) и Date.
MessagePack — это формат двоичной сериализации, предназначенный для сжатия передачи по проводу. Нет схем сообщений или проверки типа сообщения.
Бонд — это кроссплатформенная платформа для работы с схематизированными данными. Она поддерживает перекрестную сериализацию и десериализацию. Заметные отличия от других систем, перечисленных здесь, поддерживают наследование, псевдонимы типов и универсальные.
gRPC — это система RPC с открытым кодом, разработанная Google. По умолчанию он использует буферы протокола в качестве языка определения и базового формата обмена сообщениями.
Дальнейшие действия
- документация Azure Cache для Redis
- Часто задаваемые вопросы о Azure Cache для Redis
- Асинхронный шаблон на основе задач
- Документация по Redis
- StackExchange.Redis
- Руководство по секционированию данных
Связанные ресурсы
Следующие шаблоны также могут иметь отношение к вашему сценарию при реализации кэширования в приложениях:
Шаблон в сторону кэша: в этом шаблоне описывается загрузка данных по запросу в кэш из хранилища данных. Этот шаблон также помогает обеспечить согласованность между данными, которые хранятся в кэше и данных в исходном хранилище данных.
Шаблон сегментирования содержит сведения о реализации горизонтальной секционирования, чтобы повысить масштабируемость при хранении и доступе к большим объемам данных.