队列基于Stream

3 篇文章 0 订阅
1 篇文章 0 订阅

Stream 知识点

介绍

Stream是Redis 5.0版本引入的一个新的数据类型,它以更抽象的方式模拟日志数据结构,但日志仍然是完整的:就像一个日志文件,通常实现为以只附加模式打开的文件,Redis流主要是一个仅附加数据结构。至少从概念上来讲,因为Redis流是一种在内存表示的抽象数据类型,他们实现了更加强大的操作,以此来克服日志文件本身的限制。

Stream是Redis的数据类型中最复杂的,尽管数据类型本身非常简单,它实现了额外的非强制性的特性:提供了一组允许消费者以阻塞的方式等待生产者向Stream中发送的新消息,此外还有一个名为消费者组的概念。

消费者组最早是由名为Kafka(TM)的流行消息系统引入的。Redis用完全不同的术语重新实现了一个相似的概念,但目标是相同的:允许一组客户端相互配合来消费同一个Stream的不同部分的消息。

基础

为了理解Redis Stream是什么以及如何使用他们,我们将忽略所有的高级特性,从用于操纵和访问它的命令方面来专注于数据结构本身。这基本上是大多数其他Redis数据类型共有的部分,比如Lists,Sets,Sorted Sets等等。然而,需要注意的是Lists还有一个可选的更加复杂的阻塞API,由BLPOP等相似的命令导出。所以从这方面来说,Streams跟Lists并没有太大的不同,只是附加的API更复杂、更强大。

XADD消息添加

语法: XADD key ID field string [field string …]

** key:**同一类型streams的名称;

ID: streams中entry的唯一标识符,如果执行XADD命令时,传入星号(*),那么,ID会自动生成,且自动生成的ID会在执行XADD后返回,默认生成的ID格式为millisecondsTime+sequenceNumber,即当前毫秒级别的时间戳加上一个自增序号值,例如"1540013735401-0"。并且执行XADD时,不接受少于或等于上一次执行XADD的ID,否则会报错;

field&string: 接下来就是若干组field string。可以把它理解为表示属性的json中的key-value。例如,某一streams的key命名为userInfo,且某个用户信息为{“topic”:“msg”, “data”:“123456”},那么执行XADD命令如下:


本地:0>XADD test_stream * topic msg data 123456
"1606209928484-0"

XDEL消息删除

语法: XDEL key ID [ID …]

key: 同一类型streams的名称

ID: 消息ID。从streams中删除若干个entry,并且会返回实际删除数


本地:0>XDEL test_stream 1606209928484-0
"1"

XLEN消息长度

语法: XLEN key

key: 同一类型streams的名称

本地:0>XLEN test_stream
"4"

XRANGE消息读

语法: XRANGE key start end [COUNT count]

key: 同一类型streams的名称

start: 小的ID

end: 大的ID

count: 截取数量

特别说明: 有两个特殊的ID用符号"-“和”+“表示,符号”-“表示最小的ID,符号”+"表示最大的ID:


本地:0>XRANGE test_stream 1606210520309-0 1606210521078-0 
 1)    1)   "1606210520309-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


 2)    1)   "1606210521078-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


本地:0>XRANGE test_stream 1606210520309-0 1606210521078-0  count 1
 1)    1)   "1606210520309-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


本地:0>XRANGE test_stream  - + 
 1)    1)   "1606210520309-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


 2)    1)   "1606210521078-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


 3)    1)   "1606210521918-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


 4)    1)   "1606210522662-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


本地:0>XRANGE test_stream - +  count 1
 1)    1)   "1606210520309-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "123456"


XREVRANGE消息读取

用法: XREVRANGE key end start [COUNT count]

说明:
这个命令和XRANGE相反,返回一个逆序范围。end参数是更大的ID,start参数是更小的ID

XREAD消息读取

用法: XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]

[COUNT count]: 用于限定获取的消息数量

[BLOCK milliseconds]: 用于设置XREAD为阻塞模式,默认为非阻塞模式;milliseconds=0时候表示一直阻塞;

key [key …]: 多个key监听

ID [ID …]: 多个key对应多个ID 用于设置由哪个消息ID开始读取。使用0表示从第一条消息开始。(本例中就是使用0)此处需要注意,消息队列ID是单调递增的,所以通过设置起点,可以向后读取。在阻塞模式中,可以使用 , 表 示 最 新 的 消 息 I D 。 ( 在 非 阻 塞 模 式 下 ,表示最新的消息ID。(在非阻塞模式下 ID无意义)

说明: XRED读消息时分为阻塞和非阻塞模式,使用BLOCK选项可以表示阻塞模式,需要设置阻塞时长。非阻塞模式下,读取完毕(即使没有任何消息)立即返回,而在阻塞模式下,若读取不到内容,则阻塞等待。


本地:0>XREAD streams test_stream 0
 1)    1)   "test_stream"
  2)      1)        1)     "1606210520309-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"


   2)        1)     "1606210521078-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"


   3)        1)     "1606210521918-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"


   4)        1)     "1606210522662-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"


本地:0>XREAD count 2 streams test_stream 0
 1)    1)   "test_stream"
  2)      1)        1)     "1606210520309-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"


   2)        1)     "1606210521078-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"
     
     

# 说明BLOCK为0表示一致等待知道有新的数据,否则永远不会超时。并且ID的值我们用特殊字符`$`表示,这个特殊字符表示我们只获取最新添加的消息。

本地:0>XREAD BLOCK 0 STREAMS test_stream $
 1)    1)   "test_stream"
  2)      1)        1)     "1606213335389-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "123456"




XGROUP创建消费组

用法: XGROUP [CREATE key groupname id-or- ] [ S E T I D k e y i d − o r − ][SETID key id-or- ][SETIDkeyidor] [DESTROY key groupname][DELCONSUMER key groupname consumername]

key: 队里名称

groupname: 消费组名称

**id-or- : ∗ ∗ 最 后 一 个 参 数 是 要 考 虑 已 传 递 的 流 中 最 后 一 项 的 I D , 特 殊 的 I D ‘ :** 最后一个参数是要考虑已传递的流中最后一项的ID,特殊的ID ‘ :ID,ID’(这表示:流中最后一项的ID)。 在这种情况下,从该消费者组获取数据的消费者只能看到到达流的新元素。
但如果你希望消费者组获取整个流的历史记录,使用0作为消费者组的开始ID。


# 创建与队列关联的新消费者组(一个队列可以创建多个消费组)

本地:0>XGROUP CREATE test_stream test_stream_group $
"OK"

# 销毁对了的消费组
本地:0>XGROUP DESTROY test_stream test_stream_group
"1"

# 销毁消费队列中的消费组
本地:0> XGROUP DELCONSUMER test_stream test_stream_group myconsumer1
 "ok"
 
#  设置消费组的游标

#在消费者创建时设置下一个ID,作为XGROUP CREATE的最后一个参数。 但是使用这种形式,可以在以后修改下一个ID,而无需再次删除和创建使用者组。 例如,如果你希望消费者组中的消费者重新处理流中的所有消息,你可能希望将其下一个ID设置为0:

本地:0>XGROUP SETID test_stream test_stream_group 0
"OK"

XREADGROUP分消费组读取数据

用法: XREADGROUP GROUP group consumer [COUNT count][BLOCK milliseconds] STREAMS key [key …] ID [ID …]

group: 消费者分组名称

consumer: 消费者名称

[COUNT count]: 用于限定获取的消息数量

[BLOCK milliseconds]: 用于设置XREAD为阻塞模式,默认为非阻塞模式;milliseconds=0时候表示一直阻塞;

key [key …]: 多个key监听(队列)

ID: 可以是ID也可以是>。后面的特殊符号>表示这个消费者只接收从来没有被投递给其他消费者的消息,即新的消息。当然我们也可以指定具体的ID,例如指定0表示访问所有投递给该消费者的历史消息,指定1540081890919-1表示投递给该消费者且大于这个ID的历史消息

说明:
消费者组具备如下几个特点:

同一个消息不会被投递到一个消费者组下的多个消费者,只可能是一个消费者。
同一个消费者组下,每个消费者都是唯一的,通过大小写敏感的名字区分。
消费者组中的消费者请求的消息,一定是新的,从来没有投递过的消息。
消费一个消息后,需要用命令(XACK)确认,意思是说:这条消息已经给成功处理。正因为如此,当访问streams的历史消息时,每个消费者只能看到投递给它自己的消息。

# 消费者1
本地:0>XREADGROUP group test_stream_group test_stream_consumer1 count 1 streams test_stream >
 1)    1)   "test_stream"
  2)      1)        1)     "1606274137794-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "1"



# 消费者1
本地:0>XREADGROUP group test_stream_group test_stream_consumer1 count 1 streams test_stream >
 1)    1)   "test_stream"
  2)      1)        1)     "1606274140838-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "2"



# 消费者2
本地:0>XREADGROUP group test_stream_group test_stream_consumer2 count 1 streams test_stream >
 1)    1)   "test_stream"
  2)      1)        1)     "1606274145227-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "3"



# 消费者3
本地:0>XREADGROUP group test_stream_group test_stream_consumer3 count 1 streams test_stream >
 1)    1)   "test_stream"
  2)      1)        1)     "1606274149707-0"
    2)          1)      "topic"
     2)      "msg"
     3)      "data"
     4)      "4"

#多个消费者 读取多个消息,每个消息至多被消费一次 不会重复发送到不同的消费者去。

XACK消息消费确认

用法: XACK key group ID [ID …]

key: 队列名称

group: 消费组名

ID: msgID

注意:

XACK命令用于队列的消费者组的待处理条目列表(简称PEL)中删除一条或多条消息


XACK test_stream test_stream_group 1606274137794-0
"1"

XPENDING待处理条目列表(PEL)

用法: XPENDING key group [start end count] [consumer]

key: 队列名称

group: 消费组名

start: 开始消息ID。 -:表示最小的ID

end: 结束消息ID。 +:表示最大的ID

count: 截取数量

consumer: 消费者名称


本地:0>XPENDING test_stream test_stream_group
 1)  "4"
 2)  "1606274137794-0"
 3)  "1606274149707-0"
 4)    1)      1)    "test_stream_consumer1"
   2)    "2"

  2)      1)    "test_stream_consumer2"
   2)    "1"

  3)      1)    "test_stream_consumer3"
   2)    "1"


本地:0>XPENDING test_stream test_stream_group - + 10 
 1)    1)   "1606274140838-0"
  2)   "test_stream_consumer1"
  3)   "1674543"
  4)   "1"

 2)    1)   "1606274145227-0"
  2)   "test_stream_consumer2"
  3)   "1650716"
  4)   "1"

 3)    1)   "1606274149707-0"
  2)   "test_stream_consumer3"
  3)   "1640694"
  4)   "1"


本地:0>XPENDING test_stream test_stream_group  -  + 10   test_stream_consumer1
 1)    1)   "1606274137794-0"
  2)   "test_stream_consumer1"
  3)   "956954"
  4)   "1"

 2)    1)   "1606274140838-0"
  2)   "test_stream_consumer1"
  3)   "943672"
  4)   "1"

XINFO消费队列信息

用法: XINFO [CONSUMERS key groupname] key key [HELP]

案列说明:

# 查看 队列信息
本地:0>XINFO stream test_stream
 1)  "length"
 2)  "10"
 3)  "radix-tree-keys"
 4)  "1"
 5)  "radix-tree-nodes"
 6)  "2"
 7)  "groups"
 8)  "1"
 9)  "last-generated-id"
 10)  "1606274163111-0"
 11)  "first-entry"
 12)    1)   "1606274137794-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "1"


 13)  "last-entry"
 14)    1)   "1606274163111-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "10"

# 查看消费组信息
本地:0>XINFO groups test_stream
 1)    1)   "name"
  2)   "test_stream_group"
  3)   "consumers"
  4)   "3"
  5)   "pending"
  6)   "3"
  7)   "last-delivered-id"
  8)   "1606274149707-0"
  
  
# 查看组下面的消费者信息
本地:0>XINFO CONSUMERS test_stream test_stream_group
 1)    1)   "name"
  2)   "test_stream_consumer1"
  3)   "pending"
  4)   "1"
  5)   "idle"
  6)   "1314284"

 2)    1)   "name"
  2)   "test_stream_consumer2"
  3)   "pending"
  4)   "1"
  5)   "idle"
  6)   "2313074"

 3)    1)   "name"
  2)   "test_stream_consumer3"
  3)   "pending"
  4)   "1"
  5)   "idle"
  6)   "2303052"


XCLAIM消息转移到消费者中

用法: XCLAIM key group consumer min-idle-time ID [ID …] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [FORCE] [JUSTID]

案列说明:

# 消费者test_stream_consumer1中Pendin中的数据还未被ack
本地:0>XPENDING test_stream test_stream_group - + 10  test_stream_consumer1
 1)    1)   "1606274140838-0"
  2)   "test_stream_consumer1"
  3)   "8497749" #注意IDLE时间
  4)   "1" #读取次数

# 转移超过3600s的消息1606274140838-0到消费者test_stream_consumer1的Pending列表
本地:0>XCLAIM test_stream test_stream_group test_stream_consumer1 3600000 1606274140838-0
 1)    1)   "1606274140838-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "2"

# 消息1606274140838-0已经转移到消费者test_stream_consumer1的Pending中。
本地:0>XPENDING test_stream test_stream_group - + 10 test_stream_consumer1
 1)    1)   "1606274140838-0"
  2)   "test_stream_consumer1"
  3)   "24261"  # 注意IDLE时间,被重置了
  4)   "2"     # 注意,读取次数也累加了1# 消息1606274140838-0 转移到消费者test_stream_consumer2的Pending中 从原来消费者test_stream_consumer1的Pending移除。
本地:0>XCLAIM test_stream test_stream_group test_stream_consumer2 3600 1606274140838-0
 1)    1)   "1606274140838-0"
  2)      1)    "topic"
   2)    "msg"
   3)    "data"
   4)    "2"

# 从原来消费者test_stream_consumer1的Pending移除。

本地:0>XPENDING test_stream test_stream_group - + 10 test_stream_consumer2
 1)    1)   "1606274140838-0"
  2)   "test_stream_consumer2"
  3)   "7525"
  4)   "3"

 2)    1)   "1606274145227-0"
  2)   "test_stream_consumer2"
  3)   "9691677"
  4)   "1"

本地:0>XPENDING test_stream test_stream_group - + 10 test_stream_consumer1


PHP实例

<?php


namespace App\Helpers\StreamMq;


use Hyperf\Redis\RedisFactory;
use Psr\Container\ContainerInterface;

class Base
{
    /**
     * @var  \Redis
     */
    private $redis;

    public function __construct(ContainerInterface $container)
    {
        $this->redis = $container->get(RedisFactory::class)->get('default');
    }

    /**
     * 队列推送数据
     * @param string $queue 队列名称
     * @param string $topic 标识
     * @param array $data 数据
     * @return mixed msgId
     * @throws \Exception
     */
    public function push(string $queue, array $data, string $topic = 'default')
    {
        $msgId = $this->redis->rawCommand('xadd', $queue, '*', 'topic', $topic, 'data', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        if (empty($msgId)) {
            throw new \Exception('操作失败请重新操作', 9000);
        }
        return $msgId;
    }

    /**
     * 从队列中删除
     * @param string $queue 队里名称
     * @param string $msgId 消息Id(消息加入redis队列中返回的消息Id)
     * @return bool
     */
    public function delete(string $queue, string $msgId)
    {
        $res = $this->redis->rawCommand('xdel', $queue, $msgId);
        if ($res == 0) {
            return false;
        }
        return true;
    }

    /**
     * 队列长度
     * @param string $queue 队列名称
     * @return mixed
     */
    public function len(string $queue)
    {
        return $this->redis->rawCommand('XLEN', $queue);
    }

    public function getConsumerGroup($queue)
    {
        return $this->redis->rawCommand('XINFO', 'GROUPS', $queue);
    }

    /**
     * 添加消费组
     * @param string $queue 队列名称
     * @param string $consumerGroupName 消费者组名
     * @param string $type $:从尾部开始消费 0从头部开始消费
     * @return mixed
     */
    public function addConsumerGroup(string $queue, $consumerGroupName = '', $type = '$')
    {
        if (empty($consumerGroupName)) {
            $consumerGroupName = $this->getConsumerGroupName($queue);
        }
        $res = $this->redis->rawCommand('XGROUP', 'CREATE', $queue, $consumerGroupName, $type);
        if ($res == false) {
            return false;
        }
        return true;
    }

    /**
     * 消费分组名称
     * @param string $queue
     * @return string
     */
    public function getConsumerGroupName(string $queue)
    {
        return 'consumerGroupName_' . $queue;
    }

    /**
     * 删除消费者组
     * @param string $queue
     * @param  $consumerGroupName
     * @return mixed
     */
    public function delConsumerGroup(string $queue, $consumerGroupName = '')
    {
        if (empty($consumerGroupName)) {
            $consumerGroupName = $this->getConsumerGroupName($queue);
        }
        $res = $this->redis->rawCommand('XGROUP', 'DESTROY', $queue, $consumerGroupName);
        if (empty($res)) {
            return false;
        }
        return true;
    }

    /**
     * 获取消息
     * @param string $queue 队列名称
     * @param int $num 条数
     * @param string $type 类型'0-0'从头开始取'$'从尾部开始取()
     * @return mixed
     */
    public function getMsg(string $queue, int $num, $type = '$')
    {
        return $this->redis->rawCommand('XREAD', 'count', $num, 'streams', $queue, $type);
    }

    /**
     * 消费者消费
     * @param string $queue 队列
     * @param $consumerName
     * @return mixed
     */
    public function consumerMsg(string $queue, $consumerName)
    {
        $consumerGroupName = $this->getConsumerGroupName($queue);
        $consumerGroupArr = $this->getConsumerGroup($queue);
        $hasConsumerGroup = false;
        if (!empty($consumerGroupArr)) {
            foreach ($consumerGroupArr as $vo) {
                if (isset($vo[1]) && $vo[1] == $consumerGroupName) {
                    $hasConsumerGroup = true;
                }
            }
        }
        // 如果队列中中没有这个消费组则 添加消费组
        if ($hasConsumerGroup === false) {
            $this->addConsumerGroup($queue, '', '0');
        }
        $resMsg = $this->redis->rawCommand('XREADGROUP', 'GROUP', $consumerGroupName, $consumerName, 'count', '1', 'streams', $queue, '>');
        if (empty($resMsg)) {
            return '';
        }
        $msgId = $resMsg[0][1][0][0];
        $this->ack($queue, $msgId);
        return $resMsg[0][1][0];
    }


    /**
     * 消息确认
     * @param string $queue 队列名称
     * @param string $msgId 消息ID
     * @return mixed
     */
    public function ack(string $queue, string $msgId)
    {
        $consumerGroupName = $this->getConsumerGroupName($queue);
        return $this->redis->rawCommand('XACK', $queue, $consumerGroupName, $msgId);
    }

    /**
     * 获取Pending列表
     * @param string $queue 队列名称
     * @param int $num 获取条数(详情)
     * @param mixed $consumer 消费者名称
     * @return mixed
     */
    public function getPendingDetailLists(string $queue, int $num = 0, $consumer = '')
    {
        $params = [];
        $consumerGroupName = $this->getConsumerGroupName($queue);
        $params[] = $queue;
        $params[] = $consumerGroupName;
        if ($num > 0) {
            $params[] = '-';
            $params[] = '+';
            $params[] = $num;
        }
        if (!empty($consumer)) {
            $params[] = $consumer;
        }

        $res = call_user_func_array([$this->redis, 'XPENDING'], $params);
        return $res;
    }


}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值