Ответ 1
Начиная с Redis 3.2, команда SPOP
имеет аргумент [count]
для извлечения нескольких элементов из набора.
Существует ли структура данных 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 будет выполнять команды только в том случае, если просмотренные ключи не были изменены, что позволяет использовать механизм проверки и настройки.
Начиная с Redis 3.2, команда SPOP
имеет аргумент [count]
для извлечения нескольких элементов из набора.
Используйте LRANGE
с LTRIM
в конвейере . Трубопровод будет работать как одна атомная транзакция. Ваше беспокойство выше WATCH
, EXEC
здесь не применимо, потому что вы выполняете LRANGE
и LTRIM
как одну транзакцию, не имея возможности для каких-либо других транзакций от других клиентов, находящихся между ними. Попробуйте.
Чтобы расширить ответ 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
.
если вы хотите 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
Я не знаю, разрешить ли будущую версию, но я предполагаю, что нет. Я думаю, что это будет проблема с репликацией.
Redis 4. 0+ теперь поддерживает модули, которые добавляют все новые функциональные возможности и типы данных с гораздо более быстрой и безопасной обработкой, чем сценарии Lua или конвейеры multi
/exec
.
Redis Labs, нынешний спонсор Redis, имеет полезный набор модулей расширения, называемых redex: https://github.com/RedisLabsModules/redex
Модуль rxlists
добавляет несколько операций списка, включая LMPOP
и RMPOP
чтобы вы могли RMPOP
несколько значений из списка Redis. Логика по-прежнему O (n) (в основном, делает один поп в цикле), но все, что вам нужно сделать, это установить модуль один раз и просто отправить эту пользовательскую команду. Я использую его в списках с миллионами элементов, а тысячи сразу же генерируют 500MB+ сетевого трафика без проблем.
Думаю, вам стоит взглянуть на поддержку LUA в Redis. Если вы пишете LUA script и выполняете его на redis, гарантируется, что он является атомарным (потому что Redis является однопоточным). Запросы не будут выполняться до конца вашего LUA script (т.е. Вы не можете реализовать большую задачу в LUA или redis будет медленнее).
Итак, в этом script вы добавляете SPOP и RPOP, вы можете добавлять результаты от каждой команды redis в массиве LUA, а затем возвращать массив клиенту redis.
В документации по MULTI говорится, что это оптимистичная блокировка, что означает, что она будет повторять попытку делать много вещей с помощью WATCH, пока наблюдаемое значение не будет изменено. Если у вас много записей о наблюдаемом значении, это будет медленнее, чем "пессимистическая" блокировка (как и многие базы данных SQL: POSTGRESQL, MYSQL...), которые каким-то образом "останавливают мир", чтобы запрос выполнялся первым, Пессимистическая блокировка не реализована в redis, но вы можете реализовать ее, если хотите, но она сложна и, может быть, вам она не нужна (не так много пишет об этом значении: оптимистическое должно быть достаточно).
вы, вероятно, можете попробовать 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
Вот фрагмент 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, реализованный здесь, если вы хотите посмотреть. Надеюсь, поможет!