# 二、Redis 数据类型

# 1. Redis 基本数据类型

基本数据结构包含:字符串(strings)、 散列(hashes)、 列表(lists)、 集合(sets)、 有序集合(sorted sets)五种。

# 字符串(string)

1)基本说明

字符串是 Redis 最简单的储存类型,它存储的值可以是字符串整数或者浮点数,对整个字符串或者字符串的其中一部分执行操作;对整数或者浮点数执行自增(increment)或者自减(decrement)操作。

Redis 的字符串是一个由字节组成的序列,跟 Java 里面的 ArrayList 有点类似,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是 加倍 现有的空间,如果超过 1M,扩容时一次 只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M

img

2)应用场景

字符串类型在工作中使用广泛,主要用于缓存数据,提高查询性能。比如存储登录用户信息、电商中存储商品信息、可以做计数器(想知道什么时候封锁一个IP地址(访问超过几次))等等。

3)操作指令

# 添加一条String类型数据
set key value
# 获取一条String类型数据
get key
# 添加多条String类型数据
mset key1 value1 key2 value2
# 获取多条String类型数据
mget key1 key2
# 自增(+1)
incr key
# 按照步长(step)自增
incrby key step
# 自减(-1)
decr key
# 按照步长(step)递减
decrby key step
# 删除
del key

4)实操

# 插入字符串  
>set username zhangsan  
"OK"# 获取字符串  
>get username  
"zhangsan"# 插入多个字符串  
>mset age 18 address bj  
"OK"# 获取多个字符串  
>mget username age  
 1)  "zhangsan"  
 2)  "18"# 自增  
>incr num  
"1"  
>incr num  
"2"# 自减  
>decr num  
"1"# 指定步长自增  
>incrby num 2  
"3"  
>incrby num 2  
"5"# 指定步长自减  
>decrby num 3  
"2"# 删除  
>del num  
"1"

# 散列(hash)

1)基本说明

散列相当于 Java 中的 HashMap,内部是无序字典。实现原理跟 HashMap 一致。一个哈希表有多个节点,每个节点保存一个键值对。

img

与 Java 中的 HashMap 不同的是,rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略

img

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的 hash 结构取而代之。

当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

2)应用场景

Hash 也可以同于对象存储,比如存储用户信息,与字符串不一样的是,字符串是需要将对象进行序列化(比如 JSON 序列化)之后才能保存,而 Hash 则可以将用户对象的每个字段单独存储,这样就能节省序列化和反序列的时间。如下:

img

此外还可以保存用户的购买记录,比如 key 为用户 id,field 为商品 id,value 为商品数量。同样还可以用于购物车数据的存储,比如 key 为用户 id,field 为商品 id,value 为购买数量等等。

img

3)操作指令

# 设置属性
hset keyname field1 value1 field2 value2
# 获取某个属性值
hget keyname field
# 获取所有属性值
hgetall keyname
# 删除某个属性
hdel keyname field
# 获取属性个数
hlen keyname
# 按照步长自增/自减某个属性(该属性必须是数字)
hincrby keyname field step
# 删除整个 hash
del keyname

4)实操

# 插入 hash 数据
>hset userInfo username zhangsan age 18 address bj
"3"
# 获取 hash 单条 field 数据
>hget userInfo username
"zhangsan"
>hget userInfo age
"18"
# 获取 hash 多个 field 数据
>hmget userInfo username age
1) "zhangsan"
2) "18"
# 获取 hash 所有 field 数据
>hgetall userInfo
1) "username"
2) "zhangsan"
3) "age"
4) "18"
5) "address"
6) "bj"
# 获取 hash 的 field 个数
>hlen userInfo
"3"
# 自增 hash 的某个 field
>hincrby userInfo age 2
"20"
>hincrby userInfo age 2
"22"
# 自减 hash 的某个 field(通过自增负步长来实现)
>hincrby userInfo age -2
"20"
# 删除 hash 的某个 field
>hdel userInfo age
"1"
# 删除 hash 所有数据
>del userInfo
"1"

# 列表(list)

1)基本说明

Redis 中的 lists 相当于 Java 中的 LinkedList,实现原理是一个双向链表(其底层是一个快速列表),即可以支持反向查找和遍历,更方便操作。插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

2)应用场景

lists 的应用场景非常多,可以利用它轻松实现热销榜。

可以实现工作队列(利用 lists 的 push 操作,将任务存在 lists 中,然后工作线程再用 pop 操作将任务取出进行执行 )。

可以实现最新列表,比如最新评论等。

3)操作指令

# 左进
lpush key value1 value2 value3...

# 左出
lpop key

# 右进
rpush key value1 value2 value3...

# 右出
rpop key

# 从左往右读取 start和end是下标
lrange key start end

4)实操

# 从 list 左边依次插入
>lpush student zhangsan lisi wangwu
"3"

# 从 list 右边插入
>rpush student tianqi
"4"

# 从 list 左边弹出一个
>lpop liangshan
"wangwu"

# 从 list 右边弹出一个
>rpop liangshan
"tianqi"

# 获取 list 下标 0 ~ 1 的数据(左闭右闭)
>lrange liangshan 0 1
 1) "lisi"
 2) "zhangsan"

5)注意:blpop 阻塞版获取

为什么要阻塞版本的 pop 呢,主要是为了避免轮询。

举个简单的例子如果我们用 list 来实现一个工作队列。执行任务的 thread 可以调用阻塞版本的 pop 去获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。

# 集合(set)

1)基本说明

集合类似 Java 中的 HashSet,内部实现是一个 value 永远为 null 的 HashMap,实际就是通过计算 hash 的方式来快速排重的,这 也是 set 能提供判断一个成员是否在集合内的原因。

2)应用场景

Redis 的 sets 类型是使用哈希表构造的,因此复杂度是 0(1)。它支持集合内的增删改查,并且支持多个集合间的交集、并集、 差集操作。可以利用这些集合操作,解决程序开发过程当中很多数据集合间的问题。比如计算网站独立 ip,用户画像中的用 户标签,共同好友等功能。

3)操作指令

# 添加内容
sadd key value1 value2
# 查询 key 里所有的值
smembers key
# 移除 key 里面的某个 value
srem key value
# 随机移除某个 value
spop key
# 返回两个 set 的并集
sunion key1 key2
#返回 key1 剔除交集的那部分(差集)
sdiff key1 key2
#跟 siffer 相反,返回交集
sinter key1 key2

# 有序集合(sorted set)

1)基本说明

sorted sets 是 Redis 类似于 SortedSetHashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重

内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 score 的映射,而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 score,使用 跳跃表 的结构可以获得比较高的查找效率,并且在实现上比较简单。

sorted sets 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

跳表:

image-20210412221232375

2)应用场景

主要应用于根据某个权重进行排序的队列的场景,比如游戏积分排行榜,设置优先级的任务列表,学生成绩表等。

3)操作指令

# 添加元素
zadd key score value [score value...]

# 获取集合的值并按照score从小到大排列, 最小的是最上面
zrange key start end 

# 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列, 最小的是最上面
zrangeByScore key score_min score_max 

# 删除
zrem key value 

# 获取key的集合有多少元素
zcard key

# 统计分数从小到大有多少元素 (闭区间)
zcount key score_min score_max 

# 获取value所在位置(从小到大排序,最小的是0)
zrank key value 

# 获取value所在的位置(从大到小排列, 最大的是0)
zrevrank key value

4)实操

# 插入多条数据和分数并去重及排序
>zadd rank 66 zhangsan 88 lisi 77 wangwu 99 zhaoliu
"4"

# 插入多条数据及分数并去重及排序
>zadd rank 66 zhangsan 88 lisi 77 wangwu 99 zhaoliu
"0"

# 获取下标 0 ~ 3 的数据(左闭右闭)
>zrange rank 0 3
 1)  "zhangsan"
 2)  "wangwu"
 3)  "lisi"
 4)  "zhaoliu"

# 获取分数在 77 ~ 99 之间的数据(左闭右闭)
>zrangeByScore rank 77 99
 1)  "wangwu"
 2)  "lisi"
 3)  "zhaoliu"

# 删除一条数据
>zrem rank zhaoliu
"1"

# 查询元素的个数
>zcard rank
"3"

# 统计分数在 77 ~ 88 之间的数据(左闭右闭)
>zcount rank 77 88
"2"

# 获取指定元素的下标
>zrank rank zhangsan
"0"

# 获取指定元素的下标并反转
>zrevrank rank zhangsan
"2"

# 2. Redis 高级数据类型

# 位图(bitmap)

1)基本说明

bitmap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。bitmap 支持的最大位数是 232 位,使用 512M 内存就可以存储多达 42.9 亿的字节信息。

SETBIT key offset value (offset位偏移量,从0开始)

它是由一组 bit 位组成的,每个 bit 位对应 0 和 1 两个状态,虽然内部还是采用了 String 类型存储,但 Redis 提供了一些指令用于直接操作位图,因此我们可以把它看成一个 bit 数组,数组的下表就是偏移量。

2)使用场景

它的优点是内存开销小、效率高且操作简单,很适合用于「签到」这类只有两种取值的场景。比如按月存储,一个月最多 31 天,那么我们一个用于再某一个月的签到缓存二进制就是:

00000 00000 00000 00000 00000 00000 0

当某天签到将 0 改成 1 即可。

3)操作指令

# 设置值,offset 从 0 开始
SETBIT key offset value

# 获取值
GETBIT key offset

# 统计指定字节区间 bit 为 1 的数量
BITCOUNT key [start end]

# 操作多字节位域
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP/SAT/FAIL]

#  查询指定字节区间第一个被设置成 1 的 bit 位的位置
BITPOS key bit [start] [end]

BITFIELD

BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组,并对这个数组中储存的长度不同的整数进行访问(被储存的整数无需进行对齐)。换句话说,通过这个命令,用户可以执行诸如“对偏移量 1234 上的 5 位长有符号整数进行设置”、“获取偏移量 4567 上的 31 位长无符号整数”等操作。此外,BITFIELD 命令还可以对指定的整数执行加法操作和减法操作,并且这些操作可以通过设置妥善地处理计算时出现的溢出情况。

BITFIELD 命令可以在一次调用中同时对多个位范围进行操作:它接受一系列待执行的操作作为参数,并返回一个数组作为回复,数组中的每个元素就是对应操作的执行结果。

比如以下命令就展示了如何对位于偏移量 100 的 8 位长有符号整数执行加法操作,并获取位于偏移量 0 上的 4 位长无符号整数:

> BITFIELD mykey INCRBY i8 1001 GET u4 0
1)(integer)1
2)(integer)0

注意:

  • 使用 GET 子命令对超出字符串当前范围的二进制位进行访问(包括键不存在的情况),超出部分的二进制位的值将被当做是 0 。
  • 使用 SET 子命令或者 INCRBY 子命令对超出字符串当前范围的二进制位进行访问将导致字符串被扩大,被扩大的部分会使用值为 0 的二进制位进行填充。在对字符串进行扩展时,命令会根据字符串目前已有的最远端二进制位,计算出执行操作所需的最小长度。

以下是 BITFIELD 命令支持的子命令:

  • GET <type> <offset> —— 返回指定的二进制位范围。
  • SET <type> <offset> <value> —— 对指定的二进制位范围进行设置,并返回它的旧值。
  • INCRBY <type> <offset> <increment> —— 对指定的二进制位范围执行加法操作,并返回它的旧值。用户可以通过向 increment 参数传入负值来实现相应的减法操作。

除了以上三个子命令之外,还有一个子命令,它可以改变之后执行的 INCRBY 子命令在发生溢出情况时的行为:

  • OVERFLOW [WRAP|SAT|FAIL]

当被设置的二进制位范围值为整数时,用户可以在类型参数的前面添加 i 来表示有符号整数,或者使用 u 来表示无符号整数。比如说,我们可以使用 u8 来表示 8 位长的无符号整数,也可以使用 i16 来表示 16 位长的有符号整数。

BITFIELD 命令最大支持 64 位长的有符号整数以及 63 位长的无符号整数,其中无符号整数的 63 位长度限制是由于 Redis 协议目前还无法返回 64 位长的无符号整数而导致的。

二进制位和位置偏移量

在二进制位范围命令中,用户有两种方法来设置偏移量:

  • 如果用户给定的是一个没有任何前缀的数字,那么这个数字指示的就是字符串以零为开始(zero-base)的偏移量。
  • 另一方面,如果用户给定的是一个带有 # 前缀的偏移量,那么命令将使用这个偏移量与被设置的数字类型的位长度相乘,从而计算出真正的偏移量。

比如说,对于以下这个命令来说:

BITFIELD mystring SET i8 #0 100 i8 #1 200

命令会把 mystring 键里面,第一个 i8 长度的二进制位的值设置为 100 ,并把第二个 i8 长度的二进制位的值设置为 200 。当我们把一个字符串键当成数组来使用,并且数组中储存的都是同等长度的整数时,使用 # 前缀可以让我们免去手动计算被设置二进制位所在位置的麻烦。

溢出控制

用户可以通过 OVERFLOW 命令以及以下展示的三个参数,指定 BITFIELD 命令在执行自增或者自减操作时,碰上向上溢出(overflow)或者向下溢出(underflow)情况时的行为:

  • WRAP :使用回绕(wrap around)方法处理有符号整数和无符号整数的溢出情况。对于无符号整数来说,回绕就像使用数值本身与能够被储存的最大无符号整数执行取模计算,这也是 C 语言的标准行为。对于有符号整数来说,上溢将导致数字重新从最小的负数开始计算,而下溢将导致数字重新从最大的正数开始计算。比如说,如果我们对一个值为 127i8 整数执行加一操作,那么将得到结果 -128
  • SAT :使用饱和计算(saturation arithmetic)方法处理溢出,也即是说,下溢计算的结果为最小的整数值,而上溢计算的结果为最大的整数值。举个例子,如果我们对一个值为 120i8 整数执行加 10 计算,那么命令的结果将为 i8 类型所能储存的最大整数值 127 。与此相反,如果一个针对 i8 值的计算造成了下溢,那么这个 i8 值将被设置为 -127
  • FAIL :在这一模式下,命令将拒绝执行那些会导致上溢或者下溢情况出现的计算,并向用户返回空值表示计算未被执行。

需要注意的是,OVERFLOW 子命令只会对紧随着它之后被执行的 INCRBY 命令产生效果,这一效果将一直持续到与它一同被执行的下一个 OVERFLOW 命令为止。在默认情况下,INCRBY 命令使用 WRAP 方式来处理溢出计算。

以下是一个使用 OVERFLOW 子命令来控制溢出行为的例子:

> BITFIELD mykey incrby u2 1001 OVERFLOW SAT incrby u2 1021
1)(integer)1
2)(integer)1
 
> BITFIELD mykey incrby u2 1001 OVERFLOW SAT incrby u2 1021
1)(integer)2
2)(integer)2
 
> BITFIELD mykey incrby u2 1001 OVERFLOW SAT incrby u2 1021
1)(integer)3
2)(integer)3
 
> BITFIELD mykey incrby u2 1001 OVERFLOW SAT incrby u2 1021
1)(integer)0--使用默认的 WRAP 方式处理溢出
2)(integer)3--使用 SAT 方式处理溢出

而以下则是一个因为 OVERFLOW FAIL 行为而导致子命令返回空值的例子:

> BITFIELD mykey OVERFLOW FAIL incrby u2 1021
1)(nil)

作用

BITFIELD 命令的作用在于它能够将很多小的整数储存到一个长度较大的位图中,又或者将一个非常庞大的键分割为多个较小的键来进行储存,从而非常高效地使用内存,使得 Redis 能够得到更多不同的应用 ——特别是在实时分析领域:BITFIELD 能够以指定的方式对计算溢出进行控制的能力,使得它可以被应用于这一领域。

性能注意事项

BITFIELD 在一般情况下都是一个快速的命令,需要注意的是,访问一个长度较短的字符串的远端二进制位将引发一次内存分配操作,这一操作花费的时间可能会比命令访问已有的字符串花费的时间要长。

二进制位的排列

BITFIELD 把位图第一个字节偏移量 0 上的二进制位看作是 most significant 位,以此类推。举个例子,如果我们对一个已经预先被全部设置为 0 的位图进行设置,将它在偏移量 7 的值设置为 5 位无符号整数值 23 (二进制位为 10111 ),那么命令将生产出以下这个位图表示:

+--------+--------+
|00000001|01110000|
+--------+--------+

当偏移量和整数长度与字节边界进行对齐时,BITFIELD 表示二进制位的方式跟大端表示法(big endian)一致,但是在没有对齐的情况下,理解这些二进制位是如何进行排列也是非常重要的。

返回值

BITFIELD 命令的返回值是一个数组,数组中的每个元素对应一个被执行的子命令。需要注意的是,OVERFLOW 子命令本身并不产生任何回复。

4)实操

模仿一个签到功能。

# 一个用于在 2021 年 8 月第一个签到了
SETBIT user:sign:5:202108 0 1

# 检查某个用户在 2021 年 8 月 3 号是否签到了
GETBIT user:sign:5:202108 2

# 统计某个用户在 2021 年 8 月签到了多少次
BITCOUNT user:sign:5:202108

# 获取某个用户在 2021 年 8 月首次签到
BITPOS user:sign:5:202108 1

# 获取某个用户在 2021 年 8 月首次漏签
BITPOS user:sign:5:202108 0

# 获取偏移量 0 的 3 位无符号整数
BITFIELD user:sign:5:202108 get u3 0

# 基数统计(hyperloglog)

1)基本说明

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

什么是基数?比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素个数)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 264 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

HyperLogLog 算法是一种非常巧妙的近似统计海量去重元素数量的算法。它内部维护了 16384 个桶(bucket)来记录各自桶的元素数量。当一个元素到来时,它会散列到其中一个桶,以一定的概率影响这个桶的计数值。因为是概率算法,所以单个桶的计数值并不准确,但是将所有的桶计数值进行调合均值累加起来,结果就会非常接近真实的计数值。

640?wx_fmt=png

2)原理

  • 去重

    首先,这里的关键问题尽量降低代价的去重。显然,可以用哈希,hash(user_id) 就把 string 变成整数,每个 user_id 都能唯一确定一个整数(可能有冲突),这样就去重了。然后再看看怎么统计;

  • 统计

    既然是基于概率的统计方法。我们想想抛硬币,反面记 0,正面记 1,正反面的概率都是 1/2。第一次出现正面的位置记为 ρ(x),那么 ρ(0001)=3,0001 出现的概率是 1/24=1/16。换句话讲,就是进行 16次 实验,很可能出现一次或以上 0001。再换句话讲,进行 n 轮实验,最大 ρ(x) 为 y,那么可以估算进行出 n=2y

    然后,我们只要把要去重的 key,转换成一串 01 字符串,就能套用上面的统计方法了。

    记 hash 函数的最大值为 2L,把 hash(key) 看成长度为 L 的 01 串,换句话说,hash(key) 就是进行 L 次抛硬币,并且每次只要 key 相同,抛硬币的结果就相同(去重了),然后从左到右找第一个 1 的位置就ok了。

    例如:有三个key,相当于进行三次试验

    hash(key1) = 01010110,ρ(01010110) = 2

    hash(key2) = 01110010,ρ(01110010) = 2

    hash(key3) = 00100110,ρ(00100110) = 3

    最大值是 3,所以根据概率看,有 23=8 次。可以看到,在数据量小时,误差会比较大,而且根据这个算法,统计出来的数字只会是 2 的次幂,虽然这样,但是基本思想已经掌握,接下来的就是优化了。

  • 优化

    直接用最大的 ρ(x),受随机事件的影响很大,例如如果前几次就来一个0000000000000001。

    有一个方法,可以降低这种影响,就是 分桶取平均数,例如分 4 个桶,取前两位作为桶的标志,

    hash(key1) = 01010110,ρ(01010110) = 2,bucket 01

    hash(key2) = 10110010,ρ(10110010) = 0,bucket 10

    hash(key3) = 00000011,ρ(00000011) = 5,bucket 00

    hash(key4) = 11000011,ρ(11000011) = 0,bucket 11

    bucket max ρ
    bucket 00 5
    bucket 01 2
    bucket 10 0
    bucket 11 0
    bucket avg (5+2)/4,向上取整得2

    所以估算值为 22=4,这样影响就比较小了

3)使用场景

如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?

在这里插入图片描述

如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。

但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。而 Redis 的 HyperLogLog,就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

4)操作指令

Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

但是不可以使用 HyperLogLog 指令来操纵普通的字符串,因为它需要检查对象头魔术字符串是否是 "HYLL"。

# 添加指定元素到 HyperLogLog 中
PFADD key element [element..]

# 返回给定 HyperLogLog 的基数估算值
PFCOUNT key [key...]

# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey...]

5)实操

# 插入元素
127.0.0.1:6379> PFADD hedonKey redis
(integer) 1
127.0.0.1:6379> PFADD hedonKey hello
(integer) 1
127.0.0.1:6379> PFADD hedonKey hi
(integer) 1
# 查询基数预估值
127.0.0.1:6379> PFCOUNT hedonKey
(integer) 3
127.0.0.1:6379> PFADD hedonKey2 yes
(integer) 1
127.0.0.1:6379> PFCOUNT hedonKey2
(integer) 1
# 合并
127.0.0.1:6379> PFMERGE hedonKey hedonKey2
OK
127.0.0.1:6379> PFCOUNT hedonKey
(integer) 4

# 地理位置(geo)

img

1)基本说明

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

2)基本原理

GEO 并不是一种新的数据结构,而是基于 Sorted Set 实现的。

我们在之前的文章中学过 Sorted Set 结构,该结构保存的数据形式是 key-score,即一个元素对应一个分值,默认是根据分值排序的,且可以进行范围查询。

但是经纬度是一个数据对,比如(117.25,40.60),那么Redis 是如何将经纬度转换成 score 值的呢?转换成 score 值之后,是如何保证分值相邻的元素距离也相近的呢?这一切就依赖于 GeoHash 编码。

经度的范围是 [-180,180],纬度的范围是 [-90,90],当我们对经纬度进行编码时,先对经度和纬度分别进行 GeoHash 编码,然后再合并为一个编码值。

下面我们就来介绍 GeoHash 的编码方法。

对于经度或纬度来说,GeoHash 会将其编码为一个 N位 的二进制值,其实就是通过 N 次的分区得到的,N 可以自定义。下面是具体的逻辑:

  1. 第一次分区:我们把经度范围 [-180,180] 分为两个区间 [-180,0) 和 [0,180],简称为左右区间。看当前的经度值落在哪个区间中,如果在左区间,记为一次 0,否则记为 1,这样我们就得到一位编码值了。
  2. 第二次分区:假设第一次落在了 [0,180] 区间内,我们再把该区间分为两个区间 [0,90) 和 [90,180],然后再根据落在左右区间,得到一个 0 或者 1 的编码值。
  3. 重复 N 次之后,我们就得到了 N 个编码值。纬度也是一样的逻辑,可以得到 N 个编码值。

举个具体的例子,给定经纬度 [120,40],N=5。

经度 120:

分区次数 左区间 右区间 经度120所在区间 编码值
1 [-180,0) [0,180] 1
2 [0,90) [90,180] 1
3 [90,135) [135,180] 0
4 [90,112.5) [112.5,135] 1
5 [112.5,123.75) [123.75,135] 0

纬度 40:

分区次数 左区间 右区间 经度120所在区间 编码值
1 [-90,0) [0,90] 1
2 [0,45) [45,90] 0
3 [0,22.5) [22.5,45] 1
4 [22.5,33.75) [33.75,45] 1
5 [33.75,39.375) [39.375,45] 1

分别得到了经度和纬度的 N 位编码值后,是如何合并为一个编码值的呢?

规则就是:最终编码值的长度是 2N,其中偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值(从 0开始计数,0 为偶数),示意图如下:

img

使用了GeoHash编码后,经纬度 [120,40] 就被编码成了 1110011101,这个值就可以作为 key 对应的 score 值。

从上面的过程可以看出,在划分区间的过程中,我们其实是把整个空间划分成了一个一个的小方格。对经度和纬度分别做一次二分区的话,就会得到四个分区。这 4 个分区对应了 4 个方格,每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的 GeoHash 编码值基本也是接近的,如下图所示:

img

因此我们使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。

不过,有的编码值虽然在大小上接近,但实际对应的方格却距离比较远。例如,我们用 4 位来做 GeoHash 编码,把经度区间 [-180,180] 和纬度区间 [-90,90] 各分成了 4 个分区,一共 16 个分区,对应了 16 个方格。编码值为 0111 和 1000 的两个方格就离得比较远,如下图所示:

img

所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或 8 个方格。

到这里我们就了解了 Redis GEO的原理,通过 GeoHash 编码,将元素对应的经纬度编码,然后将该元素 id 作为 key,编码值作为 score 值存入 Sorted Set 中,最后通过 Sorted Set 的范围查询就可以完成我们最初的需求。

3)使用场景

GEO 支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。

GEO 的数据类型为 SORTED SET。

4)操作指令

# 添加地理位置的坐标
GEOADD key longitude latitude member [longitude latitude member ...]

# 获取地理位置的坐标
GEOPOS key member [member ...]

# 计算两个位置之间的距离
GEODIST key member1 member2 [m|km|ft|mi]

- m :米,默认单位。
- km :千米。
- mi :英里。
- ft :英尺。

# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

# 根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

- WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回
- WITHCOORD: 将位置元素的经度和维度也一并返回。
- WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
- COUNT 限定返回的记录数。
- ASC: 查找结果根据距离从近到远排序。
- DESC: 查找结果根据从远到近排序。


# 返回一个或多个位置对象的 geohash 值
GEOHASH key member [member ...]

5)实操

# 添加地理位置的坐标
127.0.0.1:6379> GEOADD hedonGeo 13.361389 38.115556 "beijing" 15.087269 37.502669 "shanghai"
(integer) 2

# 获取地理位置的坐标
127.0.0.1:6379> GEOPOS hedonGeo beijing
1) 1) "13.36138933897018433"
   2) "38.11555639549629859"
   
# 计算距离
127.0.0.1:6379> GEODIST hedonGeo beijing shanghai
"166274.1516"
127.0.0.1:6379> GEODIST hedonGeo beijing shanghai km
"166.2742"

# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
127.0.0.1:6379> GEORADIUS hedonGeo 13 37 200 km WITHDIST
1) 1) "beijing"
   2) "128.1052"
2) 1) "shanghai"
   2) "193.0633"
127.0.0.1:6379> GEORADIUS hedonGeo 13 37 200 km WITHCOORD
1) 1) "beijing"
   2) 1) "13.36138933897018433"
      2) "38.11555639549629859"
2) 1) "shanghai"
   2) 1) "15.08726745843887329"
      2) "37.50266842333162032"
127.0.0.1:6379> GEORADIUS hedonGeo 13 37 200 km WITHDIST WITHCOORD
1) 1) "beijing"
   2) "128.1052"
   3) 1) "13.36138933897018433"
      2) "38.11555639549629859"
2) 1) "shanghai"
   2) "193.0633"
   3) 1) "15.08726745843887329"
      2) "37.50266842333162032"

# 根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合
127.0.0.1:6379> GEORADIUSBYMEMBER hedonGeo beijing 100 km
1) "beijing"

# 返回位置对象的 geohash 值
127.0.0.1:6379> GEOHASH hedonGeo beijing
1) "sqc8b49rny0"

# 发布订阅(pub/sub)

1)基本说明

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

img

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

img

2)使用场景

主要是作为消息通信功能,如构建实时消息系统,普通的即时聊天,群聊等功能。

3)操作指令

# 订阅一个或多个符合给定模式的频道
PSUBSCRIBE pattern [pattern ...]

# 订阅给定的一个或多个频道的信息
SUBSCRIBE channel [channel ...]

# 查看订阅与发布系统状态
PUBSUB subcommand [argument [argument ...]]

# 将信息发送到指定的频道
PUBLISH channel message

# 退订所有给定模式的频道
PUNSUBSCRIBE [pattern [pattern ...]]

# 退订给定的频道
UNSUBSCRIBE [channel [channel ...]]

4)实操

开启第一个 Redis Client,订阅频道名为 hedonChannel 的频道:

127.0.0.1:6379> SUBSCRIBE hedonChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hedonChannel"
3) (integer) 1

再开启第二个 Redis Client,然后在同一个频道 hedonChannel 发布两次消息:

127.0.0.1:6379> PUBLISH hedonChannel "Hedonn sends message one"
(integer) 1
127.0.0.1:6379> PUBLISH hedonChannel "Hedonn sends message two"
(integer) 1

可以看到第一个 Redis Client 就收到了信息:

127.0.0.1:6379> SUBSCRIBE hedonChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hedonChannel"
3) (integer) 1
1) "message"
2) "hedonChannel"
3) "Hedonn sends message one"
1) "message"
2) "hedonChannel"
3) "Hedonn sends message two"

提问:为什么不推荐 Redis 作为消息队列?

  1. 系统稳定性:在旧版的 Redis 中,如果一个客户端订阅了某个或者某些频道,但是它读取消息的速度不够快,那么不断的积压的消息就会使得 Redis 输出缓冲区的体积越来越大,这可能会导致 Redis 的速度变慢,甚至直接崩溃。也可能会导致 Redis 被操作系统强制杀死,甚至导致操作系统本身不可用。

  2. 数据可靠性:任何网络系统在执行操作时都可能会遇到断网的情况。而断线产生的连接错误通常会使得网络连接两端中的一端进行重新连接。如果客户端在执行订阅操作的过程中断线,那么客户端将会丢失在断线期间的消息,这在很多业务场景下是不可忍受的。

上次更新: 9/25/2022, 10:25:10 PM