Stream 知识点
- 1.介绍
- 2.基础
- 3.XADD消息添加
- 4.XDEL消息删除
- 5.XLEN消息长度
- 6.XRANGE消息读取
- 7.XREVRANGE消息读取
- 8.XREAD消息读取
- 9.XGROUP创建组
- 10.XREADGROUP分消费组读取数据
- 11.XACK消息消费确认
- 12.XPENDING待处理条目列表(PEL)
- 13.XINFO消费队列信息
- 14.XCLAIM 消息转移到消费者中
- 15.PHP实例
介绍
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- ][SETIDkeyid−or−] [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;
}
}