Какой тип данных Redis подходит больше всего для следующего примера

У меня есть следующий сценарий:

  1. Получить массив чисел (из REDIS) условно
  2. Для каждого числа выполните некоторые асинхронные действия (выберите что-нибудь из БД на основе числа)
  3. Для каждой вещи в наборе результатов из БД сделайте еще один асинхронный материал

Периодически повторяйте 1. 2. 3., потому что новые числа будут постоянно добавляться в структуру REDIS. Эти числа представляют временную метку unix в миллисекундах, поэтому по умолчанию эти числа всегда будут сортироваться по времени добавления

Условно означает извлечение из REDIS тех меток времени unix, которые меньше или равны текущей метке времени unix в миллисекундах (Date.now()).

Вопрос заключается в том, какой тип данных REDIS больше всего подходит для этого варианта использования, учитывая, что этот код будет масштабироваться до N экземпляров, поэтому N экземпляров будут совместно использовать доступ к одному экземпляру REDIS. Чтобы поровну распределить нагрузку, каждый экземпляр будет считывать, например, первые (самые старые) 5 номеров из REDIS. Числа уникальны (добавление одного и того же числа должно завершиться автоматически), поэтому REDIS SET кажется хорошим выбором, но чтение M первых элементов из набора REDIS кажется невозможным.

Чтобы два разных экземпляра кода не читали одни и те же числа, операция чтения REDIS должна быть атомарной, она должна считывать числа и удалять их. Если какая-либо асинхронная операция завершается с ошибкой для определенного числа (steps 2. and 3.), необходимо снова добавить числа в REDIS для повторной обработки. Их следует повторно добавить обратно в голову не до конца, чтобы как можно скорее снова обработать их. Насколько я знаю, SADD подтолкнет его к хвосту.

SMEMBERS key читал бы всё, мне это кажется молотком. Мне нужно было бы включить некоторую логику приложения, чтобы получить первые пять, чем проверять, что меньше или равно Date.now(), а затем удалить их и каким-то образом обернуть все в одну транзакцию. Кроме того, количество элементов набора может быть огромным.
SSCAN звучит интересно, но я понятия не имею, как это работает в "масштабированной" среде, как описано выше. Кроме того, согласно документам REDIS: семейство команд SCAN предлагает только ограниченные гарантии в отношении возвращаемых элементов, поскольку коллекция, которую мы постепенно итерируем, может измениться в процессе итерации. Как описано выше, коллекция будет часто меняться.


person Srle    schedule 15.07.2016    source источник
comment
Рассматривали ли вы возможность использования Sorted Sets со скриптом Lua для атомарного извлечения на основе оценок? Если нет, с удовольствием добавим детали в качестве полноценного ответа.   -  person Itamar Haber    schedule 15.07.2016
comment
@ItamarHaber, пожалуйста, сделайте это. Пожалуйста, проверьте изменения об условиях, о которых идет речь   -  person Srle    schedule 15.07.2016


Ответы (1)


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

Соответствующими отправными точками являются ZADD, ZRANGEBYSCORE и ZREMRANGEBYSCORE.

Чтобы обеспечить атомарность при чтении и удаление участников, вы можете выбрать один из следующих вариантов: транзакции Redis, скрипт Redis Lua и в следующей версии (v4) модуль Redis.

Транзакции

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

MULTI
ZRANGEBYSCORE <keyname> -inf <now-timestamp>
ZREMRANGEBYSCORE <keyname> -inf <now-timestamp>
EXEC

Где <keyname> — имя вашего ключа, а <now-timestamp> — текущее время.

скрипт Lua

Lua-скрипт можно кэшировать и запускать на сервере, поэтому в некоторых случаях это предпочтительнее. Это определенно лучший подход для коротких фрагментов атомарной логики, если вам нужно управление потоком (помните, что транзакция MULTI возвращает значения только после выполнения). Такой скрипт будет выглядеть следующим образом:

local r = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
return r

Чтобы запустить это, сначала кэшируйте его с помощью SCRIPT LOAD, а затем вызовите его с помощью EVALSHA вот так:

EVALSHA <script-sha> 1 <key-name> <now-timestamp>

Где <script-sha> — это sha1 скрипта, возвращенный SCRIPT LOAD.

модули Redis

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

person Itamar Haber    schedule 15.07.2016
comment
очень красивое объяснение. Еще один вопрос: в предлагаемом решении, если временная метка unix является значением, какова оценка? Мне кажется, что score будет равен value не так ли? что-то вроде: ZADD <keyname> <unix-timestamp> <unix-timestamp>? - person Srle; 16.07.2016
comment
Также мне нужно использовать LIMIT с ZRANGEBYSCORE, чтобы не получать все, что меньше или равно <now-timestamp>. Поэтому в транзакции я должен использовать ZREMRANGEBYSCORE <keyname> -inf <last-timestamp-returned-by-the-limit>, но я не вижу способа получить эту последнюю метку времени. Насколько я знаю, я не могу получить промежуточные результаты от команды MULTI? (результаты первой команды в блоке MULTI для использования во второй команде в блоке MULTI) - person Srle; 16.07.2016
comment
Да, в этом случае участник и его оценка будут одинаковыми. - person Itamar Haber; 16.07.2016
comment
Требование LIMIT делает сценарий Lua правильным выбором в этом случае. - person Itamar Haber; 16.07.2016