在Redis中,如何保证在多客户端环境中从列表中获取N个项目?

在Redis中,如何保证在多客户端环境中从列表中获取N个项目?,redis,Redis,假设Redis中有一个键K,它保存着一个值列表 许多producer客户端正在使用LPUSH或RPUSH将元素逐个添加到此列表中 另一方面,另一组消费者客户端正在从列表中弹出元素,尽管有一定的限制。只有当列表中至少包含N个项目时,使用者才会尝试弹出N个项目。这样可以确保消费者在完成弹出过程后将N件物品放在手中 如果列表中包含的项目少于N个,使用者甚至不应该尝试从列表中弹出元素,因为他们的末尾至少不会有N个项目 如果只有一个消费者客户端,客户端只需运行LLEN命令检查列表是否包含至少N个项目,并使

假设Redis中有一个键K,它保存着一个值列表

许多producer客户端正在使用LPUSH或RPUSH将元素逐个添加到此列表中

另一方面,另一组消费者客户端正在从列表中弹出元素,尽管有一定的限制。只有当列表中至少包含N个项目时,使用者才会尝试弹出N个项目。这样可以确保消费者在完成弹出过程后将N件物品放在手中

如果列表中包含的项目少于N个,使用者甚至不应该尝试从列表中弹出元素,因为他们的末尾至少不会有N个项目

如果只有一个消费者客户端,客户端只需运行
LLEN
命令检查列表是否包含至少N个项目,并使用LPOP/RPOP减去N

但是,如果有多个消费者客户端,则可能存在竞争条件,并且在读取
LLEN
>=N之后,他们可以同时从列表中弹出项目。因此,我们可能最终处于这样一种状态:每个消费者弹出的元素可能少于N个,并且Redis中的列表中没有项目

使用单独的锁定系统似乎是解决此问题的一种方法,但我很好奇,这种类型的操作是否只能使用Redis命令来完成,例如Multi/Exec/Watch等


我检查了Multi/Exec方法,似乎它们不支持回滚。此外,在Multi/Exec事务之间执行的所有命令都将返回“QUEUED”,因此我无法知道我将在事务中执行的N个LPOP是否都将返回元素。

您可以使用预取器

与每位消费者贪婪地从队列中挑选商品不同,这会导致“到处都是水,但一滴也不能喝”的问题,您可以使用一个预取器来构建大小为6的数据包。当预取器有一个完整的数据包时,它可以将该项目放在一个单独的数据包队列中(另一个带有数据包列表的redis键),并在单个事务中从主队列中弹出这些项目。基本上,你写的是:

如果只有一个客户机,则该客户机可以简单地运行LLEN 命令检查列表是否至少包含N项,并减去N 使用LPOP/RPOP

如果预取器没有一个完整的数据包,它什么也不做,一直等待主队列大小达到6

消费者端,他们只需查询预取数据包队列,弹出最上面的数据包就可以了。它始终是1个预构建包(大小=6项)。如果没有可用的数据包,它们将等待

生产者方面,无需更改。他们可以继续插入主队列

顺便说一句,可以有多个预取器任务同时运行,它们可以在它们之间同步对主队列的访问

实现可伸缩的预取器 可以使用buffet表类比来描述预取器的实现。可以把排队的队伍想象成餐厅的自助餐台,客人可以在那里取食物离开。礼仪要求客人排队等候。预取程序也会遵循类似的方法。以下是算法:

Algorithm Prefetch
Begin
   while true
      check = main queue has 6 items or more    // this is a queue read. no locks required
      if(check == true)
         obtain an exclusive lock on the main queue
         if lock successful
            begin a transaction
            create a packet and fill it with top 6 items from 
              the queue after popping them
            add the packet to the prefetch queue

            if packet added to prefetch queue successfully
              commit the transaction
            else
              rollback the transaction
            end if

            release the lock
            
         else
            // someone else has the excl lock, we should just wait
            sleep for xx millisecs
         end if
      end if
   end while

End
为了简单起见,我在这里展示了一个无限轮询循环。但这可以通过使用pub/sub模式来实现。因此,预取器只需等待主队列键正在接收LPUSH的通知,然后在上面的
循环体中执行
内的逻辑


还有其他方法可以做到这一点。但是这会给你一些想法。

你可以使用预取器

与每位消费者贪婪地从队列中挑选商品不同,这会导致“到处都是水,但一滴也不能喝”的问题,您可以使用一个预取器来构建大小为6的数据包。当预取器有一个完整的数据包时,它可以将该项目放在一个单独的数据包队列中(另一个带有数据包列表的redis键),并在单个事务中从主队列中弹出这些项目。基本上,你写的是:

如果只有一个客户机,则该客户机可以简单地运行LLEN 命令检查列表是否至少包含N项,并减去N 使用LPOP/RPOP

如果预取器没有一个完整的数据包,它什么也不做,一直等待主队列大小达到6

消费者端,他们只需查询预取数据包队列,弹出最上面的数据包就可以了。它始终是1个预构建包(大小=6项)。如果没有可用的数据包,它们将等待

生产者方面,无需更改。他们可以继续插入主队列

顺便说一句,可以有多个预取器任务同时运行,它们可以在它们之间同步对主队列的访问

实现可伸缩的预取器 可以使用buffet表类比来描述预取器的实现。可以把排队的队伍想象成餐厅的自助餐台,客人可以在那里取食物离开。礼仪要求客人排队等候。预取程序也会遵循类似的方法。以下是算法:

Algorithm Prefetch
Begin
   while true
      check = main queue has 6 items or more    // this is a queue read. no locks required
      if(check == true)
         obtain an exclusive lock on the main queue
         if lock successful
            begin a transaction
            create a packet and fill it with top 6 items from 
              the queue after popping them
            add the packet to the prefetch queue

            if packet added to prefetch queue successfully
              commit the transaction
            else
              rollback the transaction
            end if

            release the lock
            
         else
            // someone else has the excl lock, we should just wait
            sleep for xx millisecs
         end if
      end if
   end while

End
为了简单起见,我在这里展示了一个无限轮询循环。但这可以通过使用pub/sub模式来实现。因此,预取器只需等待主队列键正在接收LPUSH的通知,然后在上面的
循环体中执行
内的逻辑


还有其他方法可以做到这一点。但这应该会给你一些想法。

所以你所需要的只是一种检查列表长度并有条件弹出的原子方法

这就是Lua脚本的用途,请参见命令

下面是一个Lua脚本,让您开始:

local len = redis.call('LLEN', KEYS[1])
if len >= tonumber(ARGV[1]) then
  local res = {n=len}
  for i=1,len do
    res[i] = redis.call('LPOP', KEYS[1])
  end
  return res
else
  return false
end
用作

EVAL "local len = redis.call('LLEN', KEYS[1]) \n if len >= tonumber(ARGV[1]) then \n   local res = {n=len} \n   for i=1,len do \n     res[i] = redis.call('LPOP', KEYS[1]) \n   end \n   return res \n else \n   return false \n end" 1 list 3
<