Выбирать несколько значений из структуры данных Redis атомарно?

Существует ли структура данных Redis, которая позволила бы атомизировать работу с popping (get + remove) несколькими элементами, которые она содержит?

Известны SPOP или RPOP, но они всегда возвращают одно значение. Поэтому, когда мне нужны первые N значений из набора/списка, мне нужно вызвать команду N-times, что дорого. Скажем, список set/содержит миллионы элементов. Есть ли что-то вроде SPOPM "setName" 1000, которое вернет и удалит 1000 случайных элементов из набора или RPOPM "listName" 1000, что вернет 1000 самых правых элементов из списка?

Я знаю, что есть команды типа SRANDMEMBER и LRANGE, но они не удаляют элементы из структуры данных. Их можно удалить отдельно. Однако, если клиенты читают больше из одной и той же структуры данных, некоторые элементы могут быть прочитаны более одного раза, а некоторые могут быть удалены без чтения! Поэтому атомизм - это то, о чем мой вопрос.

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

Я также знаю об отдельной поддержке транзакций. Однако это предложение от Redis docs не позволяет мне использовать его для параллельных процессов, изменяющих набор (деструктивное чтение из него):
При использовании WATCH EXEC будет выполнять команды только в том случае, если просмотренные ключи не были изменены, что позволяет использовать механизм проверки и настройки.

+27
источник поделиться
8 ответов

Начиная с Redis 3.2, команда SPOP имеет аргумент [count] для извлечения нескольких элементов из набора.

См. Http://redis.io/commands/spop#count-argument-extension

+5
источник

Используйте LRANGE с LTRIM в конвейере . Трубопровод будет работать как одна атомная транзакция. Ваше беспокойство выше WATCH, EXEC здесь не применимо, потому что вы выполняете LRANGE и LTRIM как одну транзакцию, не имея возможности для каких-либо других транзакций от других клиентов, находящихся между ними. Попробуйте.

+17
источник

Чтобы расширить ответ Eli с полным примером для коллекций списков, используйте lrange и ltrim встроенные функции вместо Lua:

127.0.0.1:6379> lpush a 0 1 2 3 4 5 6 7 8 9
(integer) 10
127.0.0.1:6379> lrange a 0 3        # read 4 items off the top of the stack
1) "9"
2) "8"
3) "7"
4) "6"
127.0.0.1:6379> ltrim a 4 -1        # remove those 4 items
OK
127.0.0.1:6379> lrange a 0 999      # remaining items
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "0"

Если вы хотите сделать операцию атомарной, вы оберните lrange и ltrim в команды multi и exec.

Также, как указано в другом месте, вам должно быть ltrim количество возвращаемых элементов, а не количество элементов, которые вы просили. например если вы сделали lrange a 0 99, но получили 50 предметов, вы бы ltrim a 50 -1 не ltrim a 100 -1.

Чтобы реализовать семантику очереди вместо стека, замените lpush на rpush.

+9
источник

если вы хотите lua script, это должно быть быстрым и легким.

local result = redis.call('lrange',KEYS[1],0,ARGV[1]-1)
redis.call('ltrim',KEYS[1],ARGV[1],-1)
return result

тогда вам не нужно зацикливаться.

обновление: Я попытался сделать это с помощью srandmember (в версии 2.6) со следующим script:

local members = redis.call('srandmember', KEYS[1], ARGV[1])
redis.call('srem', KEYS[1], table.concat(table, ' '))
return members

но я получаю сообщение об ошибке:

error: -ERR Error running script (call to f_6188a714abd44c1c65513b9f7531e5312b72ec9b): 
Write commands not allowed after non deterministic commands

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

+3
источник

Redis 4. 0+ теперь поддерживает модули, которые добавляют все новые функциональные возможности и типы данных с гораздо более быстрой и безопасной обработкой, чем сценарии Lua или конвейеры multi/exec.

Redis Labs, нынешний спонсор Redis, имеет полезный набор модулей расширения, называемых redex: https://github.com/RedisLabsModules/redex

Модуль rxlists добавляет несколько операций списка, включая LMPOP и RMPOP чтобы вы могли RMPOP несколько значений из списка Redis. Логика по-прежнему O (n) (в основном, делает один поп в цикле), но все, что вам нужно сделать, это установить модуль один раз и просто отправить эту пользовательскую команду. Я использую его в списках с миллионами элементов, а тысячи сразу же генерируют 500MB+ сетевого трафика без проблем.

+1
источник

Думаю, вам стоит взглянуть на поддержку LUA в Redis. Если вы пишете LUA script и выполняете его на redis, гарантируется, что он является атомарным (потому что Redis является однопоточным). Запросы не будут выполняться до конца вашего LUA script (т.е. Вы не можете реализовать большую задачу в LUA или redis будет медленнее).

Итак, в этом script вы добавляете SPOP и RPOP, вы можете добавлять результаты от каждой команды redis в массиве LUA, а затем возвращать массив клиенту redis.

В документации по MULTI говорится, что это оптимистичная блокировка, что означает, что она будет повторять попытку делать много вещей с помощью WATCH, пока наблюдаемое значение не будет изменено. Если у вас много записей о наблюдаемом значении, это будет медленнее, чем "пессимистическая" блокировка (как и многие базы данных SQL: POSTGRESQL, MYSQL...), которые каким-то образом "останавливают мир", чтобы запрос выполнялся первым, Пессимистическая блокировка не реализована в redis, но вы можете реализовать ее, если хотите, но она сложна и, может быть, вам она не нужна (не так много пишет об этом значении: оптимистическое должно быть достаточно).

0
источник

вы, вероятно, можете попробовать lua script (script.lua) следующим образом:

local result = {}
for i = 0 , ARGV[1] do
    local val = redis.call('RPOP',KEYS[1])
    if val then
        table.insert(result,val)
    end
end
return result

вы можете называть это следующим образом:

redis-cli  eval "$(cat script.lua)" 1 "listName" 1000
0
источник

Вот фрагмент redis-py Python, который может достичь этого, используя redis-py и pipe:

from redis import StrictRedis

client = StrictRedis()

def get_messages(q_name, prefetch_count=100):
    pipe = client.pipeline()
    pipe.lrange(q_name, 0, prefetch_count - 1)  # Get msgs (w/o pop)
    pipe.ltrim(q_name, prefetch_count, -1)  # Trim (pop) list to new value
    messages, trim_success = pipe.execute()
    return messages

Я думал, что я мог бы просто сделать для цикла pop но это было бы неэффективно, даже с конвейером, особенно если очередь списка меньше, чем prefetch_count. У меня есть полный класс RedisQueue, реализованный здесь, если вы хотите посмотреть. Надеюсь, поможет!

0
источник

Посмотрите другие вопросы по меткам или Задайте вопрос