【问题标题】:Pop multiple values from Redis data structure atomically?以原子方式从 Redis 数据结构中弹出多个值?
【发布时间】:2014-01-04 11:37:40
【问题描述】:

是否有一个 Redis 数据结构,它允许对它包含的多个元素进行弹出(get+remove)的原子操作?

有众所周知的 SPOP 或 RPOP,但它们总是返回单个值。因此,当我需要来自 set/list 的前 N ​​个值时,我需要调用命令 N 次,这很昂贵。假设集合/列表包含数百万个项目。有没有像SPOPM "setName" 1000 这样的东西,它会从集合中返回并删除 1000 个随机项或RPOPM "listName" 1000,它会从列表中返回 1000 个最右边的项?

我知道有像 SRANDMEMBER 和 LRANGE 这样的命令,但它们不会从数据结构中删除项目。它们可以单独删除。但是,如果有更多的客户端从同一个数据结构中读取,有些项目可以被多次读取,有些可以不读取就删除!因此,原子性是我的问题所在。

另外,如果这种操作的时间复杂度更高,我很好。我怀疑它会比向 Redis 服务器发出 N 个(假设是 1000,前一个示例中的 N 个)单独请求更昂贵。

我也知道单独的事务支持。但是,Redis 文档中的这句话不鼓励我将它用于修改集合的并行进程(破坏性地从中读取):
When using WATCH, EXEC will execute commands only if the watched keys were not modified, allowing for a check-and-set mechanism.

【问题讨论】:

    标签: data-structures redis time-complexity atomic


    【解决方案1】:

    pipeline 中使用LRANGELTRIM。管道将作为一个原子事务运行。您对WATCHEXEC 的上述担心在这里不适用,因为您将LRANGELTRIM 作为一个事务运行,而无法让来自任何其他客户端的任何其他事务在它们之间进行。试试看。

    【讨论】:

    • redis 管道是否保证原子性?我想你的意思是一个 redis transaction.
    • 没有MULTIEXEC 的流水线本身不是,但我曾经使用过的每个Redis 库都默认启用这些,我不想让问题复杂化。所以,是的,如果你使用一些奇怪的 Redis 库,其中管道默认没有 MULTIEXEC,你应该打开它们。
    • 我担心以下问题:假设列表的成员少于 100 个,并且您执行 lrange 0 99 后跟 ltrim 100 -1 - 第一个命令将失败,第二个命令将截断列表一无所有。
    • @Emil 第一个命令不会失败。它将返回最多 100 个成员,即使该数量更少。在此处查看文档:redis.io/commands/LRANGE#out-of-range-indexes
    • 这会阻止任何推送操作吗?这是在使用反应式框架时推荐的方法吗?
    【解决方案2】:

    我认为你应该看看 Redis 中的 LUA 支持。如果你写一个LUA脚本并在redis上执行,保证它是原子的(因为redis是单线程的)。在您的 LUA 脚本结束之前不会执行任何查询(即:您不能在 LUA 中实现大任务,否则 redis 会变慢)。

    因此,在此脚本中,您可以添加 SPOP 和 RPOP,例如,您可以将每个 redis 命令的结果附加到一个 LUA 数组中,然后将该数组返回给您的 redis 客户端。

    文档中关于 MULTI 的说法是它是乐观锁定,这意味着它将重试使用 WATCH 执行多项操作,直到未修改监视的值。如果您对监视值进行了多次写入,它将比“悲观”锁定(如许多 SQL 数据库:POSTGRESQL、MYSQL ......)慢,后者以某种方式“停止世界”以便首先执行查询.悲观锁在redis中没有实现,但是你想实现也可以,但是它很复杂,可能你不需要它(这个值上写的不多:乐观应该就够了)。

    【讨论】:

    • 当您可以使用内置的 Redis 函数在单个事务/管道中执行此操作时,为什么 Lua 会带来额外的麻烦?
    • @Eli 因为它可能是某种悲观锁定,所以它与 MULTI 完全不同。
    • 另外:你不能在多个redis请求中使用一个redis查询的返回来构建下一个。您可以轻松使用 LUA。
    • 您没有使用返回的一个查询来构建下一个。您只需 LRANGE 和 LTRIM 就可以了。您不需要从 LRANGE 到 LTRIM 的结果。
    • 在他的情况下,他不需要使用 WATCH,只需要使用 EXEC 和 MULTI,它不需要锁定任何东西,而是作为原子操作执行,保证两者之间不会发生任何事情LRANGE 和 LTRIM。 redis.io/topics/transactions
    【解决方案3】:

    您可能可以尝试这样的 lua 脚本 (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
    

    【讨论】:

    • 这个答案忽略了问题的整个基础:“因此,当我需要来自 set/list 的前 N ​​个值时,我需要调用命令 N 次,这很昂贵。假设 set /list 包含数百万个项目。”
    • 真的,也许是个误会,但目标是在命令中从 List/set 中获取 N 项? “是否有一个 Redis 数据结构,它允许对它包含的多个元素进行弹出(获取 + 删除)的原子操作?” ...“这将返回并从集合或 RPOPM 中删除 1000 个随机项目“listName”1000”... = redis-cli eval“$(cat script.lua)”1“listName”1000
    【解决方案4】:

    如果你想要一个 lua 脚本,这应该又快又简单。

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

    那么你就不必循环了。

    更新: 我尝试使用以下脚本对 srandmember(在 2.6 中)执行此操作:

    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中,这对于这种情况完全可以:MULTI; LRANGE key 0 N-1; LTRIM N -1; EXEC;
    • OP 似乎对使用事务不感兴趣 - 请参阅问题的结尾。但无论如何,您应该将您的建议添加为答案,而不是对我的答案发表评论。
    • 你能试试这个 redis.call('srem', KEYS[1], unpack(members))
    【解决方案5】:

    从 Redis 3.2 开始,命令 SPOP 有一个 [count] 参数,用于从集合中检索多个元素。

    http://redis.io/commands/spop#count-argument-extension

    【讨论】:

    • 由于 OP 要求设置 或列表 并且 SPOP 仅适用于设置,我添加了一个完整示例来说明如何为列表执行此操作:stackoverflow.com/a/43130793/213983跨度>
    【解决方案6】:

    使用 lrangeltrim 内置函数而不是 Lua 来扩展 Eli 的响应,并提供一个完整的列表集合示例:

    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 包装在 multiexec 命令中。

    同样如其他地方所述,您可能应该ltrim退回的物品数量而不是您要求的物品数量。例如如果你做了lrange a 0 99 但得到了 50 件物品,你会ltrim a 50 -1 而不是ltrim a 100 -1

    要实现队列语义而不是堆栈,请将lpush 替换为rpush

    【讨论】:

    • 您可能需要考虑更改“从堆栈顶部弹出 4 个项目”->“从堆栈顶部读取 4 个项目”。 'pop' 的整体含义是一次读取 + 删除,大部分时间从结构的后面开始。
    • 你不能在multiexec中使用lrangeltrim,因为redis不支持事务内依赖
    • 我希望我仍然能得到答案......是否有一个选项可以从 redis 列表中以原子方式“spop count”?并且以确保数据从最旧到最新返回的方式。 Spop 返回随机值,这可能会使最旧的数据在很晚的阶段得到处理。
    【解决方案7】:

    Redis 4.0+ now supports modules 添加各种新功能和数据类型,处理速度比 Lua 脚本或 multi/exec 管道更快、更安全。

    Redis 目前的赞助商 Redis Labs 有一组有用的扩展模块,称为 redexhttps://github.com/RedisLabsModules/redex

    rxlists 模块添加了几个列表操作,包括 LMPOPRMPOP,因此您可以从 Redis 列表中自动弹出多个值。逻辑仍然是 O(n) (基本上是在循环中执行一次弹出),但您所要做的就是安装一次模块并发送该自定义命令。我在包含数百万个项目和数千个项目的列表中使用它,同时生成 500MB+ 的网络流量而没有问题。

    【讨论】:

      【解决方案8】:

      这是一个 python sn-p 可以使用redis-py 和管道实现此目的:

      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 的 for 循环,但这不会有效,即使使用管道,特别是如果列表队列小于 prefetch_count。如果你想看的话,我有一个完整的 RedisQueue 类实现here。希望对您有所帮助!

      【讨论】:

        【解决方案9】:

        从 Redis 6.2 开始,您可以使用 count 参数来确定要从列表中弹出多少元素。 count 可用于LPOPRPOP。这是实现计数功能的pull request

        redis> rpush foo a b c d e f g
        (integer) 7
        redis> lrange foo 0 -1
        1) "a"
        2) "b"
        3) "c"
        4) "d"
        5) "e"
        6) "f"
        7) "g"
        redis> lpop foo
        "a"
        redis> lrange foo 0 -1
        1) "b"
        2) "c"
        3) "d"
        4) "e"
        5) "f"
        6) "g"
        redis> lpop foo 3
        1) "b"
        2) "c"
        3) "d"
        redis> lrange foo 0 -1
        1) "e"
        2) "f"
        3) "g"
        redis> rpop foo 2
        1) "g"
        2) "f"
        redis> 
        

        【讨论】:

          猜你喜欢
          • 2018-08-12
          • 2019-06-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-05-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多