Безопасная установка ключей с помощью StackExchange.Redis при разрешении удаления

Я пытаюсь использовать Redis в качестве кеша, который находится перед базой данных SQL. На высоком уровне я хочу реализовать эти операции:

  1. Прочитайте значение из Redis, если его там нет, сгенерируйте значение с помощью запроса SQL и вставьте его в Redis, чтобы нам не пришлось вычислять его снова.
  2. Запишите значение в Redis, потому что мы только что внесли некоторые изменения в нашу базу данных SQL и знаем, что, возможно, мы уже кэшировали их, и теперь они недействительны.
  3. Удалите значение, потому что мы знаем, что значение в Redis устарело, мы подозреваем, что оно никому не понадобится, но сейчас слишком много работы, чтобы пересчитывать его. Мы можем позволить следующему клиенту, который выполнит операцию № 1, вычислить ее снова.

Моя задача — понять, как реализовать № 1 и № 3, если я попытаюсь сделать это с помощью StackExchange.Redis. Если я наивно реализую # 1 с простым чтением ключа и отправкой, вполне возможно, что между тем, как я вычисляю значение из SQL и вставляю его, может произойти любое количество других операций SQL, а также попытаться отправить свои значения в Redis. через №2 или №3. Например, рассмотрим такой порядок:

  1. Клиент №1 хочет выполнить операцию №1 [Чтение] сверху. Пытается прочитать ключ, видит, что его нет.
  2. Клиент №1 обращается к базе данных SQL для генерации значения.
  3. Клиент №2 что-то делает с SQL, а затем выполняет операцию №2 [Запись] выше. Он помещает некоторое новое вычисленное значение в Redis.
  4. Клиент № 3 приходит долго, выполняет какую-то другую операцию в SQL и хочет выполнить операцию № 3 [Удалить] в Redis, зная, что если там что-то закешировано, это больше недействительно.
  5. Клиент №1 отправляет свое (теперь устаревшее) значение в Redis.

Итак, как мне реализовать операцию №1? Redis предлагает примитив WATCH, который позволяет довольно легко сделать это на голом железе, где я мог бы наблюдать другие вещи, происходящие с ключом от клиента № 1, но это не поддерживается StackExchange.Redis из-за того, как он мультиплексирует команды. Условных операций здесь недостаточно, поскольку, если я попытаюсь сказать «нажимать, только если ключ не существует», это не предотвратит гонку, как я объяснил выше. Есть ли шаблон/лучшая практика, которая используется здесь? Это кажется довольно распространенным шаблоном, который люди хотели бы реализовать.

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


person Jason Malinowski    schedule 21.01.2016    source источник


Ответы (1)


Это похоже на вопрос о правильной стратегии инвалидации кеша, а не на вопрос о Redis. Почему я так думаю? Redis WATCH/MULTI является своего рода оптимистичной стратегией блокировки, и этот тип блокировки не подходит для большинства случаев с кешем, где Запрос на чтение базы данных может быть проблемой, которая решается с помощью кеша. В описании операции №3 вы пишете:

Сейчас слишком много работы, чтобы пересчитывать. Мы можем позволить следующему клиенту, который выполнит операцию № 1, вычислить ее снова.

Таким образом, мы можем продолжить с read update случаем в качестве стратегии обновления. Вот еще несколько вопросов, прежде чем мы продолжим:

  1. Что происходит, когда 2 клиента начинают выполнять операцию №1? Оба они не могут найти значение в Redis и выполнить SQL-запрос, а затем оба запишут его в Redis. Значит, у нас должны быть гарантии, что только один клиент будет обновлять кеш?
  2. Как мы можем быть уверены в правильной последовательности операций записи (операция 3)?

Почему не оптимистическая блокировка

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

Вы можете прочитать об этапах транзакций OCC в Википедии, но в нескольких словах:

Если нет конфликта - вы обновляете свои данные. Если есть конфликт, разрешите его, как правило, прервав транзакцию и перезапустив ее, если все еще нужно обновить данные.

Redis WATCH/MULTY — это своего рода оптимистичная блокировка, поэтому они не могут вам помочь — вы не знаете, что ваш кеш-ключ был изменен, прежде чем пытаться работать с ними.

Что работает?

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

В большинстве высоконагруженных систем побеждает доступность. Это значит для кеширования? Обычно такой случай:

  1. Каждый ключ кэша содержит некоторые метаданные о значении — состоянии, версии и времени жизни. Последнее не Redis TTL - обычно, если ваш ключ должен находиться в кеше в течение X времени, время жизни в метаданных имеет время X + Y, там Y есть время, чтобы гарантировать обновление процесса.
  2. Вы никогда не удаляете ключ напрямую - вам нужно просто обновить состояние или время жизни.
  3. Каждый раз, когда ваше приложение считывает данные из кеша, если должно принять решение - если данные имеют состояние "действительно" - используйте его. Если данные имеют статус «недействительный», попробуйте обновить или использовать устаревшие данные.

Как обновлять при чтении (весьма важно, что это «ручная» смесь оптимистичной и песситической блокировки):

  1. Попробуйте установить пессимистическую блокировку (в Redis с помощью SETEX — подробнее здесь).
  2. Если не удалось - верните устаревшие данные (помните, что нам все еще нужна доступность).
  3. В случае успеха выполните SQL-запрос и запишите в кеш.
  4. Прочитайте версию из Redis еще раз и сравните с версией, прочитанной ранее.
  5. Если версия такая же - отметьте состояние как "действительное".
  6. Освободить замок.

Как аннулировать (ваши операции №2, №3):

  1. Увеличьте версию кэша и установите состояние «недействительное».
  2. Обновите время жизни/ttl, если это необходимо.

Почему так сложно

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

У меня очередь!

Извините, вы раньше не сказали - я бы не стал все это писать. Если есть очередь, то все становится проще:

  1. Каждая операция модификации должна помещать задание в очередь.
  2. Только асинхронный рабочий должен выполнять SQL и обновлять ключ.
  3. Вам по-прежнему нужно использовать «состояние» (действительное/недействительное) для ключа кеша, чтобы разделить логику приложения с кешем.

Это ответ?

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

person misterion    schedule 21.01.2016