内存数据库Redis7-Redis命令(二)

3.10 集合的底层实现原理

Redis 中对于 Set 类型的底层实现,直接采用了 hashTable。但对于 Hash、ZSet、List 集合的底层实现进行了特殊的设计,使其保证了 Redis 的高性能。

3.10.1 两种实现的选择

对于Hash与ZSet集合,其底层的实现实际有两种:压缩列表zipList,与跳跃列表skipList。这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。只有同时满足以配置文件 redis.conf 中相关集合元素数量阈值与元素大小阈值两个条件,使用的就是压缩列表 zipList,只要有一个条件不满足使用的就是跳跃列表 skipList。例如,对于

ZSet 集合中这两个条件如下:

  • 集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128
  • 每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64字节
3.10.2 zipList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yir0ftNz-1677219889079)(photo/image-20230224093121898.png)]

什么是 zipList

zipList,通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构由三部分构成:head、entries 与 end。这三部分在内存上是连续存放的。

head

head 又由三部分构成:

  • zlbytes:占 4 个字节,用于存放 zipList 列表整体数据结构所占的字节数,包括 zlbytes本身的长度。

  • zltail:占 4 个字节,用于存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾 entry 位置,以方便操作。

  • zllen:占 2 字节,用于存放列表包含的 entry 个数。由于其只有 16 位,所以 zipList 最多可以含有的 entry 个数为 2 16 -1 = 65535 个。

entries

entries 是真正的列表,由很多的列表元素 entry 构成。由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。

每个 entry 由三部分构成:

  • prevlength:该部分用于记录上一个 entry 的长度,以实现逆序遍历。默认长度为 1 字节,只要上一个 entry 的长度<254 字节,prevlength 就占 1 字节,否则其会自动扩展为 3 字节长度。

  • encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding

  • 固定长度为 1 字节。如果 data 为字符串类型,则 encoding 长度可能会是 1 字节、2 字节或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。

  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。

end

end 只包含一部分,称为 zlend。占 1 个字节,值固定为 255,即二进制位为全 1,表示一个 zipList 列表的结束。

3.10.3 listPack

对于 ziplist,实现复杂,为了逆序遍历,每个 entry 中包含前一个 entry 的长度,这样会导致在 ziplist 中间修改或者插入 entry 时需要进行级联更新。在高并发的写操作场景下会极度降低 Redis 的性能。为了实现更紧凑、更快的解析,更简单的实现,重写实现了 ziplist,并命名为 listPack。

在 Redis 7.0 中,已经将 zipList 全部替换为了 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U2gIsTQg-1677219889080)(photo/image-20230224093342410.png)]

什么是 listPack

listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也由三部分构成:head、entries 与 end,且这三部分在内存上也是连续存放的。

listPack与zipList的重大区别在head与每个entry的结构上,表示列表结束的end与zipList的 zlend 是相同的,占一个字节,且 8 位全为 1。

head

head 由两部分构成:

  • totalBytes:占 4 个字节,用于存放 listPack 列表整体数据结构所占的字节数,包括totalBytes 本身的长度。

  • elemNum:占 2 字节,用于存放列表包含的 entry 个数。其意义与 zipList 中 zllen 的相同。

与 zipList 的 head 相比,没有了记录最后一个 entry 偏移量的 zltail。

entries

entries 也是 listPack 中真正的列表,由很多的列表元素 entry 构成。由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。但与 zipList 的 entry 结构相比,listPack的 entry 结构发生了较大变化。

其中最大的变化就是没有了记录前一个 entry 长度的 prevlength,而增加了记录当前entry 长度的 element-total-len。而这个改变仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入 entry 时引发的级联更新。

每个 entry 仍由三部分构成:

  • encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding长度可能会是 1、2、3、4、5 或 9 字节。不同的字节长度,其标识位不同。如果 data为字符串类型,则 encoding 长度可能会是 1、2 或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。

  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字

    节长度不同。

  • element-total-len:该部分用于记录当前 entry 的长度,用于实现逆序遍历。由于其特殊的记录方式,使其本身占有的字节数据可能会是 1、2、3、4 或 5 字节。

3.10.4 skipList

什么是 skipList

skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。

skipList 原理

假设有一个带头尾结点的有序链表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmpCZJaY-1677219889081)(photo/image-20230224093702341.png)]

在该链表中,如果要查找某个数据,需要从头开始逐个进行比较,直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点,或者找到最后尾结点,后两种都属于没有找到的情况。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入位置。

为了提升查找效率,在偶数结点上增加一个指针,让其指向下一个偶数结点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-goS6Qi8J-1677219889082)(photo/image-20230224093721466.png)]

这样所有偶数结点就连成了一个新的链表(简称高层链表),当然,高层链表包含的节点个数只是原来链表的一半。此时再想查找某个数据时,先沿着高层链表进行查找。当遇到第一个比待查数据大的节点时,立即从该大节点的前一个节点回到原链表中进行查找。例如,若想插入一个数据 20,则先在(8,19,31,42)的链表中查找,找到第一个比 20 大的节点 31,然后再在高层链表中找到 31 节点的前一个节点 19,然后再在原链表中获取到其下一个节点值为 23。比 20 大,则将 20 插入到 19 节点与 23 节点之间。若插入的是 25,比节点23 大,则插入到 23 节点与 31 节点之间。

该方式明显可以减少比较次数,提高查找效率。如果链表元素较多,为了进一步提升查找效率,可以将原链表构建为三层链表,或再高层级链表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcyFJNCm-1677219889083)(photo/image-20230224093750000.png)]

层级越高,查找效率就会越高。

存在的问题

这种对链表分层级的方式从原理上看确实提升了查找效率,但在实际操作时就出现了问题:由于固定序号的元素拥有固定层级,所以列表元素出现增加或删除的情况下,会导致列表整体元素层级大调整,但这样势必会大大降低系统性能。

例如,对于划分两级的链表,可以规定奇数结点为高层级链表,偶数结点为低层级链表。对于划分三级的链表,可以按照节点序号与 3 取模结果进行划分。但如果插入了新的节点,或删除的原来的某些节点,那么定会按照原来的层级划分规则进行重新层级划分,那么势必会大大降低系统性能。

算法优化

为了避免前面的问题,skipList 采用了随机分配层级方式。即在确定了总层级后,每添加一个新的元素时会自动为其随机分配一个层级。这种随机性就解决了节点序号与层级间的固定关系问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3F2zbC8-1677219889083)(photo/image-20230224093841084.png)]

上图演示了列表在生成过程中为每个元素随机分配层级的过程。从这个 skiplist 的创建和插入过程可以看出,每一个节点的层级数都是随机分配的,而且新插入一个节点不会影响到其它节点的层级数。只需要修改插入节点前后的指针,而不需对很多节点都进行调整。这就降低了插入操作的复杂度。

skipList 指的就是除了最下面第 1 层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。在查找数据的时先在高层级链表中进行查找,然后逐层降低,最终可能会降到第 1 层链表来精确地确定数据位置。在这个过程中由于跳过了一些节点,从而加快了查找速度。

3.10.5 quickList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oVfNZlsS-1677219889084)(photo/image-20230224093914890.png)]

什么是 quickList

quickList,快速列表,quickList 本身是一个双向无循环链表,它的每一个节点都是一个zipList。从Redis3.2版本开始,对于List的底层实现,使用quickList替代了zipList 和 linkedList。zipList 与 linkedList 都存在有明显不足,而 quickList 则对它们进行了改进:吸取了 zipList 和 linkedList 的优点,避开了它们的不足。quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。当然,对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size属性可以指定。

检索操作

为了更深入的理解 quickList 的工作原理,通过对检索、插入、删除等操作的实现分析来加深理解。

对于 List 元素的检索,都是以其索引 index 为依据的。quickList 由一个个的 zipList 构成,每个 zipList 的 zllen 中记录的就是当前 zipList 中包含的 entry 的个数,即包含的真正数据元素的个数。根据要检索元素的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个zipList 中。

插入操作

由于 zipList 是有大小限制的,所以在 quickList 中插入一个元素在逻辑上相对就比较复杂一些。假设要插入的元素的大小为 insertBytes,而查找到的插入位置所在的 zipList 当前的大小为 zlBytes,那么具体可分为下面几种情况:

  • 情况一:当 insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置即可
  • 情况二:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的首部位置,此时需要查看该 zipList 的前一个 zipList 的大小 prev_zlBytes。
    • 若 insertBytes + prev_zlBytes<= list-max-ziplist-size 时,直接将元素插入到前一个zipList 的尾部位置即可
    • 若 insertBytes + prev_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
  • 情况三:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的尾部位置,此时需要查看该 zipList 的后一个 zipList 的大小 next_zlBytes。
    • 若 insertBytes + next_zlBytes<= list-max-ziplist-size 时,直接将元素插入到后一个zipList 的头部位置即可
    • 若 insertBytes + next_zlBytes> list-max-ziplist-size 时,直接将元素自己构建为一个新的 zipList,并连入 quickList 中
  • 情况四:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的中间位置,则将当前 zipList 分割为两个 zipList 连接入 quickList 中,然后将元素插入到分割后的前面 zipList 的尾部位置

删除操作

对于删除操作,只需要注意一点,在相应的 zipList 中删除元素后,该 zipList 中是否还有元素。如果没有其它元素了,则将该 zipList 删除,将其前后两个 zipList 相连接。

3.10.6 key value 中元素的数量

前面讲述的 Redis 的各种特殊数据结构的设计,不仅极大提升了 Redis 的性能,并且还使得 Redis 可以支持的 key 的数量、集合 value 中可以支持的元素数量可以非常庞大。

  • Redis 最多可以处理 2 32个 key(约 42 亿),并且在实践中经过测试,每个 Redis 实例至少可以处理 2.5 亿个 key。
  • 每个 Hash、List、Set、ZSet 集合都可以包含 2 32 个元素。

3.11 BitMap 操作命令

3.11.1 BitMap 简介

BitMap 是 Redis 2.2.0 版本中引入的一种新的数据类型。该数据类型本质上就是一个仅包含 0 和 1 的二进制字符串。而其所有相关命令都是对这个字符串二进制位的操作。用于描述该字符串的属性有三个:key、offset、bitValue。

  • key:BitMap 是 Redis 的 key-value 中的一种 Value 的数据类型,所以该 Value 一定有其对应的 key。
  • offset:每个 BitMap 数据都是一个字符串,字符串中的每个字符都有其对应的索引,该索引从 0 开始计数。该索引就称为每个字符在该 BitMap 中的偏移量 offset。这个 offset的值的范围是[0,2 32 -1],即该 offset 的最大值为 4G-1,即 4294967295,42 亿多。
  • bitValue:每个 BitMap 数据中都是一个仅包含 0 和 1 的二进制字符串,每个 offset 位上的字符就称为该位的值 bitValue。bitValue 的值非 0 即 1。
3.11.2 setbit
  • 格式:SETBIT key offset value
  • 功能:为给定 key 的BitMap 数据的 offset 位置设置值为 value。其返回值为修改前该 offset位置的 bitValue
  • 说明:对于原 BitMap 字符串中不存在的 offset 进行赋值,字符串会自动伸展以确保它可以将 value 保存在指定的 offset 上。当字符串值进行伸展时,空白位置以 0 填充。当然,设置的 value 只能是 0 或 1。不过需要注意的是,对使用较大 offset 的 SETBIT 操作来说,内存分配过程可能造成 Redis 服务器被阻塞。

演示代码如下:

127.0.0.1:6379> setbit Texas 1056 1
(integer) 0
127.0.0.1:6379> setbit Texas 1057 1
(integer) 0
127.0.0.1:6379> setbit Texas 1058 0
(integer) 0
3.11.3 getbit

格式:GETBIT key offset

功能:对 key 所储存的 BitMap 字符串值,获取指定 offset 偏移量上的位值 bitValue。

说明:当 offset 比字符串值的长度大,或者 key 不存在时,返回 0 。

演示代码如下:

127.0.0.1:6379> getbit Texas 1056
(integer) 1
127.0.0.1:6379> getbit Texas 1057
(integer) 1
127.0.0.1:6379> getbit Texas 1058
(integer) 0

3.11.4 bitcount

格式:BITCOUNT key [start] [end]

功能:统计给定字符串中被设置为 1 的 bit 位的数量。一般情况下,统计的范围是给定的整个 BitMap 字符串。但也可以通过指定额外的 start 或 end 参数,实现仅对指定字节范围内字符串进行统计,包括 start 和 end 在内。注意,这里的 start 与 end 的单位是字节,不是 bit,并且从 0 开始计数。

说明:start 和 end 参数都可以使用负数值: -1 表示最后一个字节, -2 表示倒数第二个字节,以此类推。另外,对于不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行BITCOUNT 操作,结果为 0 。

演示代码如下:

127.0.0.1:6379> bitcount Texas
(integer) 2
127.0.0.1:6379> bitcount Texas 0 3
(integer) 0
3.11.5 bitpos

格式:BITPOS key bit [start] [end]

功能:返回 key 指定的 BitMap 中第一个值为指定值 bit(非 0 即 1) 的二进制位的位置。pos,即 position,位置。在默认情况下, 命令将检测整个 BitMap,但用户也可以通过可选的 start 参数和 end 参数指定要检测的范围。

说明:start 与 end 的意义与 bitcount 命令中的相同。

演示代码如下:

127.0.0.1:6379> bitpos Texas 1
(integer) 1056
127.0.0.1:6379> bitpos Texas 0
(integer) 0

3.11.6 bitop

  • 格式:BITOP operation destkey key [key …]

  • 功能:对一个或多个 BitMap 字符串 key 进行二进制位操作,并将结果保存到 destkey 上。

  • operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:

    • BITOP AND destkey key [key …] :对一个或多个 BitMap 执行按位与操作,并将结果保存到 destkey 。

    • BITOP OR destkey key [key …] :对一个或多个 BitMap 执行按位或操作,并将结果保存到 destkey 。

    • BITOP XOR destkey key [key …] :对一个或多个 BitMap 执行按位异或操作,并将结果保存到 destkey 。

    • BITOP NOT destkey key :对给定 BitMap 执行按位非操作,并将结果保存到 destkey 。

  • 说明:

    • 除了 NOT 操作之外,其他操作都可以接受一个或多个 BitMap 作为输入。

    • 除了 NOT 操作外,其他对一个 BitMap 的操作其实就是一个复制。

    • 如果参与运算的多个 BitMap 长度不同,较短的 BitMap 会以 0 作为补充位与较长BitMap 运算,且运算结果长度与较长 BitMap 的相同。

演示代码如下:

127.0.0.1:6379[2]> setbit aaa 0 1
(integer) 0
127.0.0.1:6379[2]> setbit aaa 3 1
(integer) 0
127.0.0.1:6379[2]> setbit bbb 0 1
(integer) 0
127.0.0.1:6379[2]> setbit bbb 1 1
(integer) 0
127.0.0.1:6379[2]> setbit bbb 3 1
(integer) 0
127.0.0.1:6379[2]> bitop and ccc aaa bbb
(integer) 1
127.0.0.1:6379[2]> bitcount ccc
(integer) 2
127.0.0.1:6379[2]> getbit ccc 0
(integer) 1
127.0.0.1:6379[2]> getbit ccc 1
(integer) 0
127.0.0.1:6379[2]> getbit ccc 2
(integer) 0
127.0.0.1:6379[2]> getbit ccc 3
(integer) 1
3.11.7 应用场景

由于 offset 的取值范围很大,所以其一般应用于大数据量的二值性统计。例如平台活跃用户统计(二值:访问或未访问)、支持率统计(二值:支持或不支持)、员工考勤统计(二值:上班或未上班)、图像二值化(二值:黑或白)等。

不过,对于数据量较小的二值性统计并不适合 BitMap,可能使用 Set 更为合适。当然,具体多少数据量适合使用 Set,超过多少数据量适合使用 BitMap,这需要根据具体场景进行具体分析。

例如,一个平台要统计日活跃用户数量。

如果使用 Set 来统计,只需上线一个用户,就将其用户 ID 写入 Set 集合即可,最后只需统计出 Set 集合中的元素个数即可完成统计。即 Set 集合占用内存的大小与上线用户数量成正比。假设用户 ID 为 m 位 bit 位,当前活跃用户数量为 n,则该 Set 集合的大小最少应该是m*n 字节。

如果使用 BitMap 来统计,则需要先定义出一个 BitMap,其占有的 bit 位至少为注册用户数量。只需上线一个用户,就立即使其中一个 bit 位置 1,最后只需统计出 BitMap 中 1 的个数即可完成统计。即 BitMap 占用内存的大小与注册用户数量成正比,与上线用户数量无关。假设平台具有注册用户数量为 N,则 BitMap 的长度至少为 N 个 bit 位,即 N/8 字节。

何时使用 BitMap 更合适?令 mn 字节 = N/8 字节,即 n = N/8/m = N/(8m) 时,使用Set 集合与使用 BitMap 所占内存大小相同。以淘宝为例,其用户 ID 长度为 11 位(m),其注册用户数量为 8 亿(N),当活跃用户数量为 8 亿/(811) = 0.09 亿 = 9106 = 900 万,使用 Set与 BitMap 占用的内存是相等的。但淘宝的日均活跃用户数量为 8 千万,所以淘宝使用 BitMap更合适

3.12 HyperLogLog 操作命令

3.12.1 HyperLogLog 简介

HyperLogLog 是 Redis 2.8.9 版本中引入的一种新的数据类型,其意义是 hyperlog log,超级日志记录。该数据类型可以简单理解为一个 set 集合,集合元素为字符串。但实际上HyperLogLog 是一种基数计数概率算法,通过该算法可以利用极小的内存完成独立总数的统计。其所有相关命令都是对这个“set 集合”的操作。

HyperLogLog 算法是由法国人 Philippe Flajolet 博士研究出来的,Redis的作者 Antirez 为了纪念 Philippe Flajolet 博士对组合数学和基数计算算法分析的研究,在设计 HyperLogLog 命令的时候使用了 Philippe Flajolet姓名的英文首字母 PF 作为前缀。遗憾的是 Philippe Flajolet 博士于 2011年 3 月 22 日因病在巴黎辞世。

HyperLogLog 算法是一个纯数学算法,我们这里不做研究。

3.12.2 pfadd

格式:PFADD key element *element …+

功能:将任意数量的元素添加到指定的 HyperLogLog 集合里面。如果内部存储被修改了返回 1,否则返回 0。

127.0.0.1:6379> pfadd courses redis nginx
(integer) 1
127.0.0.1:6379> pfadd courses zookeeper
(integer) 1
3.12.3 pfcount

格式:PFCOUNT key [key …]

功能:该命令作用于单个 key 时,返回给定 key 的 HyperLogLog 集合的近似基数;该命令作用于多个 key 时,返回所有给定 key 的 HyperLogLog 集合的并集的近似基数;如果key 不存在,则返回 0。

127.0.0.1:6379> pfcount courses
(integer) 3

127.0.0.1:6379> pfadd courses nginx
(integer) 0
127.0.0.1:6379> pfcount courses
(integer) 3
3.12.4 pfmerge

格式:PFMERGE destkey sourcekey [sourcekey …]

功能:将多个 HyperLogLog 集合合并为一个 HyperLogLog 集合,并存储到 destkey 中,合并后的 HyperLogLog 的基数接近于所有 sourcekey 的 HyperLogLog 集合的并集。

127.0.0.1:6379> pfadd mds redis kafka roketMQ
(integer) 1
127.0.0.1:6379> pfcount mds
(integer) 3
127.0.0.1:6379> pfcount courses mds
(integer) 5
127.0.0.1:6379> pfmerge sum courses mds
OK
127.0.0.1:6379> pfcount sum
(integer) 5
3.12.5 应用场景

HyperLogLog 可对数据量超级庞大的日志数据做不精确的去重计数统计。当然,这个不精确的度在 Redis 官方给出的误差是 0.81%。这个误差对于大多数超大数据量场景是被允许的。对于平台上每个页面每天的 UV 数据,非常适合使用 HyperLogLog 进行记录。

3.13 Geospatial 操作命令

3.13.1 Geospatial 简介

Geospatial,地理空间。

Redis 在 3.2 版本中引入了 Geospatial 这种新的数据类型。该类型本质上仍是一种集合,只不过集合元素比较特殊,是一种由三部分构成的数据结构,这种数据结构称为空间元素:

  • 经度:longitude。有效经度为[-180,180]。正的表示东经,负的表示西经。
  • 纬度:latitude。有效纬度为[-85.05112878,85.05112878]。正的表示北纬,负的表示南纬。
  • 位置名称:为该经纬度所标注的位置所命名的名称,也称为该 Geospatial 集合的空间元素名称。

通过该类型可以设置、查询某地理位置的经纬度,查询某范围内的空间元素,计算两空间元素间的距离等。

3.13.2 geoadd

格式:GEOADD key longitude latitude member [longitude latitude member …]

功能:将一到多个空间元素添加到指定的空间集合中。

说明:当用户尝试输入一个超出范围的经度或者纬度时,该命令会返回一个错误。

127.0.0.1:6379> geoadd capitals 116.3 39.9 beijing
(integer) 1
127.0.0.1:6379> geoadd capitals 37.35 55.45 moscow 129.69 35.69 tokyo
(integer) 2
3.13.3 geopos

格式:GEOPOS key member *member …+

功能:从指定的地理空间中返回指定元素的位置,即经纬度。

说明:因为 该命令接受可变数量元素作为输入,所以即使用户只给定了一个元素,命令也会返回数组。

127.0.0.1:6379> geopos capitals beijing
1) 1) "116.30000084638595581"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos capitals moscow tokyo
1) 1) "37.34999924898147583"
   2) "55.44999985582003887"
2) 1) "129.69000130891799927"
   2) "35.69000060704775734"
3.13.4 geodist

格式:GEODIST key member1 member2 [unit]

功能:返回两个给定位置之间的距离。其中 unit 必须是以下单位中的一种:

  • m :米,默认

  • km :千米

  • mi :英里

  • ft:英尺

说明:如果两个位置之间的其中一个不存在, 那么命令返回空值。另外,在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

127.0.0.1:6379> geodist capitals beijing moscow
"5815679.7076"
127.0.0.1:6379> geodist capitals beijing moscow km
"5815.6797"
127.0.0.1:6379> geodist capitals beijing tokyo km
"1264.9816"
3.13.5 geohash

格式:GEOHASH key member [member …]

功能:返回一个或多个位置元素的 Geohash 值。

说明:GeoHash 是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个字符串。该值主要用于底层应用或者调试, 实际中的作用并不大。

127.0.0.1:6379> geohash capitals moscow tokyo
1) "ucfe3ym7wj0"
2) "wyk5p1jmk40"
127.0.0.1:6379> geohash capitals beijing
1) "wx4dyrx4670"
3.13.6 georadius

格式:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST]

[WITHHASH] [ASC|DESC] [COUNT count]

功能:以给定的经纬度为中心,返回指定地理空间中包含的所有位置元素中,与中心距离不超过给定半径的元素。返回时还可携带额外的信息:

WITHDIST :在返回位置元素的同时,将位置元素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致。

WITHCOORD :将位置元素的经维度也一并返回。

WITHHASH:将位置元素的 Geohash 也一并返回,不过这个 hash 以整数形式表示

命令默认返回未排序的位置元素。 通过以下两个参数,用户可以指定被返回位置元素的排序方式:

  • ASC :根据中心的位置,按照从近到远的方式返回位置元素。
  • DESC :根据中心的位置,按照从远到近的方式返回位置元素。

说明:在默认情况下, 该命令会返回所有匹配的位置元素。虽然用户可以使用 COUNT 选项去获取前 N 个匹配元素,但因为命令在内部可能会需要对所有被匹配的元素进行处理,所以在对一个非常大的区域进行搜索时,即使使用 COUNT 选项去获取少量元素,该命令的执行速度也可能会非常慢。

127.0.0.1:6379> georadius capitals 116.23 39.54 6000 km
1) "moscow"
2) "beijing"
3) "tokyo"
127.0.0.1:6379> georadius capitals 116.23 39.54 6000 km withcoord withdist
1) 1) "moscow"
   2) "5839.9700"
   3) 1) "37.34999924898147583"
      2) "55.44999985582003887"
2) 1) "beijing"
   2) "40.4868"
   3) 1) "116.30000084638595581"
      2) "39.90000009167092543"
3) 1) "tokyo"
   2) "1259.3051"
   3) 1) "129.69000130891799927"
      2) "35.69000060704775734"
127.0.0.1:6379> georadius capitals 116.23 39.54 6000 km withcoord withdist count 2
1) 1) "beijing"
   2) "40.4868"
   3) 1) "116.30000084638595581"
      2) "39.90000009167092543"
2) 1) "tokyo"
   2) "1259.3051"
   3) 1) "129.69000130891799927"
      2) "35.69000060704775734"
3.13.7 georadiusbymember

格式:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST]

[WITHHASH] [ASC|DESC] [COUNT count]

功能:这个命令和 GEORADIUS 命令一样,都可以找出位于指定范围内的元素,但该命令的中心点是由位置元素形式给定的,而不是像 GEORADIUS 那样,使用输入的经纬度来指定中心点。

说明:返回结果中也是包含中心点位置元素的

127.0.0.1:6379> georadiusbymember capitals beijing 6000 km withcoord withdist
1) 1) "moscow"
   2) "5815.6797"
   3) 1) "37.34999924898147583"
      2) "55.44999985582003887"
2) 1) "beijing"
   2) "0.0000"
   3) 1) "116.30000084638595581"
      2) "39.90000009167092543"
3) 1) "tokyo"
   2) "1264.9816"
   3) 1) "129.69000130891799927"
      2) "35.69000060704775734"
3.13.8 应用场景

Geospatial 的意义是地理位置,所以其主要应用地理位置相关的计算。例如,微信发现中的“附近”功能,添加朋友中“雷达加朋友”功能;QQ 动态中的“附近”功能;钉钉中的“签到”功能等。

3.14 发布/订阅命令

3.14.1 消息系统

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clg9LgC8-1677219889085)(photo/image-20230224134355923.png)]

​ 发布/订阅,即 pub/sub,是一种消息通信模式:发布者也称为消息生产者,生产和发送消息到存储系统;订阅者也称为消息消费者,从存储系统接收和消费消息。这个存储系统可以是文件系统 FS、消息中间件 MQ、数据管理系统 DBMS,也可以是 Redis。整个消息发布者、订阅者与存储系统称为消息系统。

​ 消息系统中的订阅者订阅了某类消息后,只要存储系统中存在该类消息,其就可不断的接收并消费这些消息。当存储系统中没有该消息后,订阅者的接收、消费阻塞。而当发布者将消息写入到存储系统后,会立即唤醒订阅者。当存储系统放满时,不同的发布者具有不同的处理方式:有的会阻塞发布者的发布,等待可用的存储空间;有的则会将多余的消息丢失。

​ 当然,不同的消息系统消息的发布/订阅方式也是不同的。例如 RocketMQ、Kafka 等消息中间件构成的消息系统中,发布/订阅的消息都是以主题 Topic 分类的。而 Redis 构成的消息系统中,发布/订阅的消息都是以频道 Channel 分类的。

3.14.2 subscribe

  • 格式:SUBSCRIBE channel [channel …]

  • 功能:Redis 客户端通过一个 subscribe 命令可以同时订阅任意数量的频道。在输出了订阅了主题后,命令处于阻塞状态,等待相关频道的消息。

演示代码如下:

127.0.0.1:6379> subscribe news sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2
3.14.3 psubscribe
  • 格式:PSUBSCRIBE pattern [pattern …]

  • 功能:订阅一个或多个符合给定模式的频道。

  • 说明:这里的模式只能使用通配符 *。例如,

    it* 可以匹配所有以 it 开头的频道,像 it.news、

    it.blog、it.tweets 等;news.*可以匹配所有以 news.开头的频道,像 news.global.today、news.it 等。

演示代码如下:

127.0.0.1:6379> psubscribe news.* sports.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1
1) "psubscribe"
2) "sports.*"
3) (integer) 2
3.14.4 publish
  • 格式:PUBLISH channel message

  • 功能:Redis 客户端通过一条 publish 命令可以发布一个频道的消息。返回值为接收到该消息的订阅者数量。

演示代码如下:

127.0.0.1:6379> publish news "Huang divorced Yang"
(integer) 1
127.0.0.1:6379> publish sports "Chinese football broke out of Aisa"
(integer) 1
3.14.5 unsubscribe
  • 格式:UNSUBSCRIBE [channel [channel …]]

  • 功能:Redis 客户端退订指定的频道。

  • 说明:如果没有频道被指定,也就是一个无参数的 UNSUBSCRIBE 命令被执行,那么客户端使用 SUBSCRIBE 命令订阅的所有频道都会被退订。在这种情况下,命令会返回一个信息,告知客户端所有被退订的频道。

演示代码如下:

127.0.0.1:6379> unsubscribe news
1) "unsubscribe"
2) "news"
3) (integer) 0
3.14.6 punsubscribe
  • 格式:PUNSUBSCRIBE [pattern [pattern …]]

  • 功能:退订一个或多个符合给定模式的频道。

  • 说明:这里的模式只能使用通配符 *。如果没有频道被指定,其效果与 SUBSCRIBE 命令相同,客户端将退订所有订阅的频道。

演示代码如下:

127.0.0.1:6379> punsubscribe news.*
1) "punsubscribe"
2) "news.*"
3) (integer) 0

3.14.7 pubsub

  • 格式:PUBSUB [argument [argument …]]

  • 功能:PUBSUB 是一个查看订阅与发布系统状态的内省命令集,它由数个不同格式的子命令组成,下面分别介绍这些子命令的用法。

pubsub channels

格式:PUBSUB CHANNELS [pattern]

  • 功能:列出当前所有的活跃频道。活跃频道指的是那些至少有一个订阅者的频道。
  • 说明:pattern 参数是可选的。如果不给出 pattern 参数,将会列出订阅/发布系统中的所有活跃频道。如果给出 pattern 参数,那么只列出和给定模式 pattern 相匹配的那些活跃频道。pattern 中只能使用通配符*。

演示代码如下:

127.0.0.1:6379> pubsub channels n*
1) "news"
127.0.0.1:6379> pubsub channels s*
1) "sports"
127.0.0.1:6379> pubsub channels b*s*
(empty array)

pubsub numsub

  • 格式:PUBSUB NUMSUB [channel-1 … channel-N]
  • 功能:返回给定频道的订阅者数量。不给定任何频道则返回一个空列表。

演示代码如下:

127.0.0.1:6379> pubsub numsub news
1) "news"
2) (integer) 1

pubsub numpat

  • 格式:PUBSUB NUMPAT
  • 功能:查询当前 Redis 所有客户端订阅的所有频道模式的数量总和

演示代码如下:

127.0.0.1:6379> pubsub numpat
(integer) 2

3.15 Redis 事务

Redis 的事务的本质是一组命令的批处理。这组命令在执行过程中会被顺序地、一次性全部执行完毕,只要没有出现语法错误,这组命令在执行期间是不会被中断。

3.15.1 Redis 事务特性

Redis 的事务仅保证了数据的一致性,不具有像 DBMS 一样的 ACID 特性。

  • 这组命令中的某些命令的执行失败不会影响其它命令的执行,不会引发回滚。即不具备原子性。

  • 这组命令通过乐观锁机制实现了简单的隔离性。没有复杂的隔离级别。

  • 这组命令的执行结果是被写入到内存的,是否持久取决于 Redis 的持久化策略,与事务无关。

3.15.2 Redis 事务实现

三个命令

Redis 事务通过三个命令进行控制。

  • muti:开启事务

  • exec:执行事务

  • discard:取消事务

基本使用

下面是定义并执行事务的用法:

127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> multi //开启事务
OK
127.0.0.1:6379(TX)> set age 20
QUEUED
127.0.0.1:6379(TX)> incrby age 2
QUEUED
127.0.0.1:6379(TX)> exec //执行事务
1) OK
2) (integer) 22

事务执行后,再访问事务中定义的变量,其值是修改过后。

127.0.0.1:6379> get age
"22"

下面是定义但取消事务的举例:

127.0.0.1:6379> multi //开启事务
OK
127.0.0.1:6379(TX)> set age 30
QUEUED
127.0.0.1:6379(TX)> incr age
QUEUED
127.0.0.1:6379(TX)> discard //取消事务
OK

事务取消后,事务中的命令是没有执行的。

3.15.3 Redis 事务异常处理

语法错误

当事务中的命令出现语法错误时,整个事务在 exec 执行时会被取消。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MHIsLYNB-1677219889086)(photo/image-20230224140446703.png)]

exec 的提示是 exec 被忽略,事务被取消,因为之前的错误。

此时访问 age 的值,发现其仍为 18,并没有变为事务中设置的 20。

执行异常

如果事务中的命令没有语法错误,但在执行过程中出现异常,该异常不会影响其它命令的执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qfqYRQKu-1677219889087)(photo/image-20230224140534759.png)]

以上事务中第 2 条命令在执行时出现异常。因为 score 并非是整型,无法被增加 20 的操作。但该异常并不会影响其前后命令的正确执行。查看 score 与 name 的值,发现是执行成功的结果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0modGmkZ-1677219889088)(photo/image-20230224140612093.png)]

3.15.4 Redis 事务隔离机制

为什么需要隔离机制

在并发场景下可能会出现多个客户端对同一个数据进行修改的情况。

例如:有两个客户端 C 左与 C 右,C 左需要申请 40 个资源,C 右需要申请 30 个资源。它们首先查看了当前拥有的资源数量,即 resources 的值。它们查看到的都是 50,都感觉资源数量可以满足自己的需求,于是修改资源数量,以占有资源。但结果却是资源出现了“超卖”情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c4M3TQep-1677219889088)(photo/image-20230224142223619.png)]

为了解决这种情况,Redis 事务通过乐观锁机制实现了多线程下的执行隔离。

隔离的实现

Redis 通过 watch 命令再配合事务实现了多线程下的执行隔离。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dilh7BAT-1677219889089)(photo/image-20230224142246394.png)]

以上两个客户端执行的时间顺序为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pahqnnu4-1677219889090)(photo/image-20230224142259047.png)]

实现原理

其内部的执行过程如下:

  1. 当某一客户端对 key 执行了 watch 后,系统就会为该 key 添加一个 version 乐观锁,并初始化 version。例如初值为 1.0。

  2. 此后客户端 C 左将对该 key 的修改语句写入到了事务命令队列中,虽未执行,但其将该key 的 value 值与 version 进行了读取并保存到了当前客户端缓存。此时读取并保存的是version 的初值 1.0。

  3. 此后客户端 C 右对该 key 的值进行了修改,这个修改不仅修改了 key 的 value 本身,同时也增加了 version 的值,例如使其 version 变为了 2.0,并将该 version 记录到了该 key信息中。

  4. 此后客户端 C 左执行 exec,开始执行事务中的命令。不过,其在执行到对该 key 进行修改的命令时,该命令首先对当前客户端缓存中保存的 version 值与当前 key 信息中的version 值。如果缓存 version 小于 key 的 version,则说明客户端缓存的 key 的 value 已经过时,该写操作如果执行可能会破坏数据的一致性。所以该写操作不执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值