HyperLoglog基数统计
Redis 经常使用的数据类型有字符串、列表、散列、集合和有序集合,但这些类型并不能满足所有的应用场景,因此,Redis 的后续版本不断的扩增其他数据类型来增强 Redis 适用能力。在 Redis 2.8.9 版本中新增了 HyperLogLog 类型。
什么是HyperLoglog
HyperLoglog 是 Redis 重要的数据类型之一,它非常适用于海量数据的计算、统计,其特点是占用空间小,计算速度快。
HyperLoglog 采用了一种基数估计算法,因此,最终得到的结果会存在一定范围的误差(标准误差为 0.81%)。每个 HyperLogLog key 只占用 12 KB 内存,所以理论上可以存储大约2^64
个值,而 set(集合)则是元素越多占用的内存就越多,两者形成了鲜明的对比 。
HyperLoglog 使用起来较为方便,但是其内部原理较为复杂,不建议大家深入研究,只要会用即可。
基数定义
基数定义:一个集合中不重复的元素个数就表示该集合的基数,比如集合 {1,2,3,1,2} ,它的基数集合为 {1,2,3} ,所以基数为 3。HyperLogLog 正是通过基数估计算法来统计输入元素的基数。
HyperLoglog 不会储存元素值本身,因此,它不能像 set 那样,可以返回具体的元素值。HyperLoglog 只记录元素的数量,并使用基数估计算法,快速地计算出集合的基数是多少。
场景应用
HyperLogLog 也有一些特定的使用场景,它最典型的应用场景就是统计网站用户月活量,或者网站页面的 UV(网站独立访客)数据等。
UV 与 PV(页面浏览量) 不同,UV 需要去重,同一个用户一天之内的多次访问只能计数一次。这就要求用户的每一次访问都要带上自身的用户 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。
当一个网站拥有巨大的用户访问量时,我们可以使用 Redis 的 HyperLogLog 来统计网站的 UV (网站独立访客)数据,它提供的去重计数方案,虽说不精确,但 0.81% 的误差足以满足 UV 统计的需求。
常用命令
命令 | 说明 |
---|---|
PFADD key element [element ...] | 添加指定元素到 HyperLogLog key 中。 |
PFCOUNT key [key ...] | 返回指定 HyperLogLog key 的基数估算值。 |
PFMERGE destkey sourcekey [sourcekey ...] | 将多个 HyperLogLog key 合并为一个 key。 |
基本命令
HyperLogLog 提供了三个常用命令,分别是PFADD
、PFCOUNT
和PFMERGE
。
下面看一组实例演示:假设有 6 个用户(user01-user06),他们分别在上午 8 与 9 点访问了www.biancheng.net
C语言中文网。
#向指定的key中添加用户 127.0.0.1:6379> PFADD user:uv:2021011308 user01 user02 user03 (integer) 1 #向指定的key中添加用户 127.0.0.1:6379> PFADD user:uv:2021011309 user04 user05 (integer) 1 #统计基数值 127.0.0.1:6379> PFCOUNT user:uv:2021011308 (integer) 3 #重复元素不能添加成功,其基数仍然为3 127.0.0.1:6379> PFADD user:uv:2021011308 user01 user02 (integer) 0 127.0.0.1:6379> PFCOUNT user:uv:2021011308 (integer) 3 #添加新元素值 127.0.0.1:6379> PFADD user:uv:2021011308 user06 (integer) 1 #基数值变为4 127.0.0.1:6379> PFCOUNT user:uv:2021011308 (integer) 4 #统计两个key的基数值 127.0.0.1:6379> PFCOUNT user:uv:2021011308 user:uv:2021011309 (integer) 6 #将两个key值合并为一个 127.0.0.1:6379> PFMERGE user:uv:2021011308-09 user:uv:2021011308 user:uv:2021011309 OK #使用合并后key统计基数值 127.0.0.1:6379> PFCOUNT user:uv:2021011308-09 (integer) 6
在线练习工具:Try Redis
查看更多命令:Command reference – Redis
Redis PubSub发布订阅
Redis PubSub 模块又称发布订阅者模式,是一种消息传递系统,实现了消息多播功能。发布者(即发送方)发送消息,订阅者(即接收方)接收消息,而用来传递消息的链路则被称为 channel。在 Redis 中,一个客户端可以订阅任意数量的 channel(可译为频道)。
消息多播:生产者生产一次消息,中间件负责将消息复制到多个消息队列中,每个消息队列由相应的消费组进行消费,这是分布式系统常用的一种解耦方式。
发布/订阅流程
下面的示例演示了“发布/订阅者”模式的工作流程,示意图如下所示:
图1:发布订阅模式
1) 订阅者/等待接收消息
首先打开 Redis 客户端,然后订阅了一个名为“www.biancheng.net”的 channel,使用如下命令:
#订阅channel 127.0.0.1:6379> SUBSCRIBE www.biancheng.net Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "www.biancheng.net" 3) (integer) 1
上述示例使用SUBSCRIBE
命令订阅了名为 www.biancheng.net 的 channel。命令执行后该客户端会出处于等待接收消息的阻塞状态。
2) 发布者/发送消息
下面再启动一个 Redis 客户端,输入如下命令:
127.0.0.1:6379> PUBLISH www.biancheng.net "this is website" (integer) 1 127.0.0.1:6379> PUBLISH www.biancheng.net "hello world" (integer) 1 127.0.0.1:6379> PUBLISH www.biancheng.net "how are you" (integer) 1
通过上述PUBLISH
命令发布了三条信息。现在两个客户端在处于同一个名为“www.biancheng.net”的频道上,前者负责接收消息,后者负责发布消息。
3) 订阅者/成功接收消息
完成了上述操作后,您会在接收消息的客户端得到如下输出结果:
127.0.0.1:6379> SUBSCRIBE www.biancheng.net Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "www.biancheng.net" 3) (integer) 1 1) "message" 2) "www.biancheng.net" 3) "this is website" 1) "message" 2) "www.biancheng.net" 3) "hello world" 1) "message" 2) "www.biancheng.net" 3) "how are you"
常用命令汇总
命令 | 说明 |
---|---|
PSUBSCRIBE pattern [pattern ...] | 订阅一个或多个符合指定模式的频道。 |
PUBSUB subcommand [argument [argument ...]] | 查看发布/订阅系统状态,可选参数 1) channel 返回在线状态的频道。 2) numpat 返回指定模式的订阅者数量。 3) numsub 返回指定频道的订阅者数量。 |
PUBSUB subcommand [argument [argument ...]] | 将信息发送到指定的频道。 |
PUNSUBSCRIBE [pattern [pattern ...]] | 退订所有指定模式的频道。 |
SUBSCRIBE channel [channel ...] | 订阅一个或者多个频道的消息。 |
UNSUBSCRIBE [channel [channel ...]] | 退订指定的频道。 |
基本命令应用
下面对上述常用命令做演示:
#订阅指定模式的频道,*代表通配符,会匹配所有www开头的频道 127.0.0.1:6379> PSUBSCRIBE www* Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "www*" 3) (integer) 1 #按ctrl+c退出阻塞状态 ^C C:\Users\Administrator>redis-cli #查看发布订阅系统状态,返回相应的频道 127.0.0.1:6379> PUBSUB channels 1) "www.biancheng.net" #退订指定模式的频道 127.0.0.1:6379> PUNSUBSCRIBE www* 1) "punsubscribe" 2) "www*" 3) (integer) 0 #退订指定频道 127.0.0.1:6379> UNSUBSCRIBE www.biancheng.net 1) "unsubscribe" 2) "www.biancheng.net" 3) (integer) 0
注意:每个模式以*
作为匹配符,比如 www* 匹配所有以 www 开头的频道,比如 www.baidu.com 、www.biancheng.net 等等。
在线练习工具:Try Redis
查看更多命令:Command reference – Redis
Redis Stream消息队列
Redis Stream 是 Redis 5.0 版本引入的一种新数据类型,同时它也是 Redis 中最为复杂的数据结构,本节主要对 Stream 做相关介绍。
什么是Stream?
Stream 实际上是一个具有消息发布/订阅功能的组件,也就常说的消息队列。其实这种类似于 broker/consumer(生产者/消费者)的数据结构很常见,比如 RabbitMQ 消息中间件、Celery 消息中间件,以及 Kafka 分布式消息系统等,而 Redis Stream 正是借鉴了 Kafaka 系统。
1) 优点
Strean 除了拥有很高的性能和内存利用率外, 它最大的特点就是提供了消息的持久化存储,以及主从复制功能,从而解决了网络断开、Redis 宕机情况下,消息丢失的问题,即便是重启 Redis,存储的内容也会存在。
2) 流程
Stream 消息队列主要由四部分组成,分别是:消息本身、生产者、消费者和消费组,对于前述三者很好理解,下面了解什么是消费组。
一个 Stream 队列可以拥有多个消费组,每个消费组中又包含了多个消费者,组内消费者之间存在竞争关系。当某个消费者消费了一条消息时,同组消费者,都不会再次消费这条消息。被消费的消息 ID 会被放入等待处理的 Pending_ids 中。每消费完一条信息,消费组的游标就会向前移动一位,组内消费者就继续去争抢下消息。
Redis Stream 消息队列结构程如下图所示:
图1:Redis Stream流程处理图
下面对上图涉及的专有名词做简单解释:
- Stream direction:表示数据流,它是一个消息链,将所有的消息都串起来,每个消息都有一个唯一标识 ID 和对应的消息内容(Message content)。
- Consumer Group :表示消费组,拥有唯一的组名,使用 XGROUP CREATE 命令创建。一个 Stream 消息链上可以有多个消费组,一个消费组内拥有多个消费者,每一个消费者也有一个唯一的 ID 标识。
- last_delivered_id :表示消费组游标,每个消费组都会有一个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
- pending_ids :Redis 官方称为 PEL,表示消费者的状态变量,它记录了当前已经被客户端读取的消息 ID,但是这些消息没有被 ACK(确认字符)。如果客户端没有 ACK,那么这个变量中的消息 ID 会越来越多,一旦被某个消息被 ACK,它就开始减少。
3) ACK
ACK(Acknowledge character)即确认字符,在数据通信中,接收方传递给发送方的一种传输类控制字符。表示发来的数据已确认接收无误。在 TCP/IP 协议中,如果接收方成功的接收到数据,那么会回复一个 ACK 数据。通常 ACK 信号有自己固定的格式,长度大小,由接收方回复给发送方。
常用命令汇总
命令 | 说明 |
---|---|
XADD | 添加消息到末尾。 |
XTRIM | 对 Stream 流进行修剪,限制长度。 |
XDEL | 删除指定的消息。 |
XLEN | 获取流包含的元素数量,即消息长度。 |
XRANGE | 获取消息列表,会自动过滤已经删除的消息。 |
XREVRANGE | 反向获取消息列表,ID 从大到小。 |
XREAD | 以阻塞或非阻塞方式获取消息列表。 |
XGROUP CREATE | 创建消费者组。 |
XREADGROUP GROUP | 读取消费者组中的消息。 |
XACK | 将消息标记为"已处理"。 |
XGROUP SETID | 为消费者组设置新的最后递送消息ID。 |
XGROUP DELCONSUMER | 删除消费者。 |
XGROUP DESTROY | 删除消费者组。 |
XPENDING | 显示待处理消息的相关信息。 |
XCLAIM | 转移消息的归属权。 |
XINFO | 查看 Stream 流、消费者和消费者组的相关信息。 |
XINFO GROUPS | 查看消费者组的信息。 |
XINFO STREAM | 查看 Stream 流信息。 |
XINFO CONSUMERS key group | 查看组内消费者流信息。 |
基本命令应用
下面通过一组示例演示有关 Stream 命令的使用:
<span style="color:#444444">#添加一个消息, * 表示以时间戳自动创建id
127.0.0.1:6379> XADD mystream * username www.biancheng.net age 10 c.biancheng.net age 9
"1610619132674-1"
#自定义id等于001,注意id只增不减
127.0.0.1:6379> XADD mystream1 001 name zhangsan addr hebei
"1-0"
127.0.0.1:6379> XADD mystream1 002 name lisi addr hunan
"2-0"
#如果插入重复的id号会报错
127.0.0.1:6379> XADD mystream1 001 name wangwu addr fujian
(error) ERR The ID specified in XADD is equal or smaller than the target stream top item
127.0.0.1:6379> XADD mystream1 003 name wangwu addr fujian
"3-0"
#删除id=001的数据
127.0.0.1:6379> XDEL mystream1 001
(integer) 1
#查看stream队列包含的消息数量,也就消息长度
127.0.0.1:6379> XLEN mystream1
(integer) 2
#获取消息列表,-表示最小,+表示最大
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1610619132674-0"
2) 1) "username"
2) "www.biancheng.net"
3) "age"
4) "10"
2) 1) "1610619178028-0"
2) 1) "username"
2) "c.biancheng.net"
3) "age"
4) "9"
#获取消息列表
127.0.0.1:6379> XRANGE mystream1 - 003
1) 1) "2-0"
2) 1) "name"
2) "lisi"
3) "addr"
4) "hunan"
2) 1) "3-0"
2) 1) "name"
2) "wangwu"
3) "addr"
4) "fujian"
#使用count指定返回数据的数量
127.0.0.1:6379> XRANGE mystream1 - 003 count 1
1) 1) "2-0"
2) 1) "name"
2) "lisi"
3) "addr"
4) "hunan"
#删除整个Stream
127.0.0.1:6379> DEL mystream
#使用xread读取消息
127.0.0.1:6379> XREAD count 2 STREAMS mystream1 2-0
1) 1) "mystream1"
2) 1) 1) "3-0"
2) 1) "name"
2) "wangwu"
3) "addr"
4) "fujian"
</span>
创建消息ID
上述示例中,当我们创建一个 Srteam 时, 需要创建消息 ID,该 ID 是唯一、不可重复的,并且只增不减。消息 ID 有两种创建方式,一是系统自动生成,二是自定义创建。
1) 系统自动创建
语法格式如下:
XADD key ID field value [field value ...]
参数说明如下:
- key :指定队列名称,如果不存就创建;
- ID :消息 id,我们使用
*
表示由 redis 生成,可以自定义,但是要自己保证递增性; - field value :消息记录。
返回值是毫秒时间戳格式的字符串。比如 1610619132674-2,它表示在该毫秒内产生的第 2 条消息。使用示例:
XADD mystream * username www.biancheng.net age 10
2) 自定义ID
自定义 ID 比较简单,但是需要注意的是 ID 的形式必须是 “整数”,并且后面加入消息的 ID 必须大于前面消息的 ID,也就是自定义 ID 也必须遵守递增的规则。示例如下:
XADD mystream1 001 name zhangsan addr hebei
创建消费组
Redis Stream通过XGROUP CREATE
指令创建消费组(Consumer Group),在创建时,需要传递起始消息的 ID 用来初始化 last_delivered_id 变量。语法格式如下:
<span style="color:#444444">XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]</span>
参数说明如下:
- key :指定 Stream 队列名称,若不存在则自动创建。
- groupname :自定义消费组的名称,不可重复。
- $ :表示从尾部开始消费,只接受新消息,而当前 Stream 的消息则被忽略。
示例如下:
<span style="color:#444444">#创建消费组,并传递消息起始id 0-0
127.0.0.1:6379> XGROUP CREATE mystream1 ms1 0-0
OK
#从尾部开始消费信息,只接受新消息
127.0.0.1:6379> XGROUP CREATE mystream1 ms3 $
OK
#xinfo查看队列信息
127.0.0.1:6379> XINFO stream mystream1
#队列中消息的长度
1) "length"
2) (integer) 2
3) "radix-tree-keys"
4) (integer) 1
5) "radix-tree-nodes"
6) (integer) 2
#有几个消费组
7) "groups"
8) (integer) 2
9) "last-generated-id"
10) "3-0"
11) "first-entry" #第一个消息
12) 1) "2-0"
2) 1) "name"
2) "lisi"
3) "addr"
4) "hunan"
13) "last-entry" #最后一个消息
14) 1) "3-0"
2) 1) "name"
2) "wangwu"
3) "addr"
4) "fujian"
#查看消费组信息
127.0.0.1:6379> XINFO GROUPS mystream1
1) 1) "name"
2) "ms1"
3) "consumers"
4) (integer) 0
5) "pending"
6) (integer) 0
7) "last-delivered-id"
8) "0-0"
2) 1) "name"
2) "ms3"
3) "consumers"
4) (integer) 0
5) "pending"
6) (integer) 0
7) "last-delivered-id"
8) "3-0"
</span>
消费消息
Redis Stream 通过XREADGROUP
命令使消费组消费信息,它和XREAD
命令一样,都可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的 PLE(正在处理的消息)结构里,客户端处理完毕后使用 XACK 命令通知 Redis 服务器,本条消息已经处理完毕,该消息的 ID 就会从 PEL 中移除。示意图如下:
图2:Redis Stream 流程示意图
XREADGROUP
命令的语法格式如下所示:
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
参数说明如下:
- group :消费组名称。
- consumer :消费者名称。
- count : 要读取的数量。
- milliseconds : 阻塞时间,以毫秒为单位。
- key : 键指定的队列名称。
- ID : 表示消息 ID。
使用示例如下:
<span style="color:#444444">#消费组中消费者读取消息,> 表示每当消费一个信息,消费组游标就前移一位
127.0.0.1:6379> XREADGROUP GROUP ms1 c1 COUNT 1 STREAMS mystream1 >
1) 1) "mystream1"
2) 1) 1) "2-0"
2) 1) "name"
2) "lisi"
3) "addr"
4) "hunan"
#再使用ms1-c1读取一条消息
127.0.0.1:6379> XREADGROUP GROUP ms1 c1 COUNT 1 STREAMS mystream1 >
1) 1) "mystream1"
2) 1) 1) "3-0"
2) 1) "name"
2) "wangwu"
3) "addr"
4) "fujian"
#BLOCK 1000表示等待1秒,如果没有任何消息到来,则返回nill,此时移动到了末尾
127.0.0.1:6379> XREADGROUP GROUP ms1 c1 COUNT 1 BLOCK 1000 STREAMS mystream1 >
(nil)
127.0.0.1:6379> XREADGROUP GROUP ms1 c1 COUNT 1 STREAMS mystream1 1
1) 1) "mystream1"
2) 1) 1) "2-0"
2) 1) "name"
2) "lisi"
3) "addr"
4) "hunan"
#超出了消息id的范围
127.0.0.1:6379> XREADGROUP GROUP ms1 c1 COUNT 1 STREAMS mystream1 3
1) 1) "mystream1"
2) (empty list or set)
#添加新的消息 ID为004
127.0.0.1:6379> XADD mystream1 004 name zhangwu age 24
#使用另外一个消费组读取消息
127.0.0.1:6379> XREADGROUP GROUP ms3 c2 COUNT 2 STREAMS mystream1 >
1) 1) "mystream1"
2) 1) 1) "4-0"
2) 1) "name"
2) "zhangwu"
3) "age"
4) "21"
#xack将id=002消息标记为已经处理
127.0.0.1:6379> XACK mystream1 ms1 002
(integer) 1
</span>
注意:>
表示每当消费者读取一条消息时,last_delivered_id 变量就会前移一位。