Redis存储⑤Redis五大数据类型之 List 和 Set。

目录

[1. List 列表](#1. List 列表)

[1.1 List 列表常见命令](#1.1 List 列表常见命令)

[1.2 阻塞版本命令](#1.2 阻塞版本命令)

[1.3 List命令总结和内部编码](#1.3 List命令总结和内部编码)

[1.4 List典型使用场景](#1.4 List典型使用场景)

[1.4.1 消息队列](#1.4.1 消息队列)

[1.4.2 分频道的消息队列](#1.4.2 分频道的消息队列)

[1.4.3 微博 Timeline](#1.4.3 微博 Timeline)

[2. Set 集合](#2. Set 集合)

[2.1 Set 集合常见命令](#2.1 Set 集合常见命令)

[2.2 Set 集合间命令](#2.2 Set 集合间命令)

[2.3 Set命令小结和内部编码](#2.3 Set命令小结和内部编码)

[2.4 Set集合使用场景](#2.4 Set集合使用场景)

本篇完。


1. List 列表

列表两端插入和弹出操作:

列表类型是用来存储多个有序的字符串,如上图所示,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储个元素。

在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等,如下图所示。

列表的获取、删除等操作:

列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

列表类型的特点:

  • 列表中的元素是有序的(指的是顺序很关键,不是指升序 / 降序),这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取上图中的第 5 个元素,可以执行 lindex user:1:messages 4 或者倒数第 1 个元素,lindex user:1:messages -1 就可以得到元素 e。
  • 区分获取和删除的区别,例如上图中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的长度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度是不会变化的。
  • **列表中的元素是允许重复的,**例如下图中的列表中是包含了两个 a 元素的。

列表中允许有重复元素:


1.1 List 列表常见命令

lpush

将一个或者多个元素从左侧放入(头插)到 list 中。

**语法:**lpush key element [element ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数。

**返回值:**插入后 list 的长度。

示例:

前面的序号时专门给结果集使用的序号,和 list 下标无关。


lpushx

在 key 存在时,将一个或者多个 元素从左侧放入(头插)到 list 中。不存在,直接返回。

lpushx 指的是:left push exists

**语法:**lpushx key element [element ...]

**命令有效版本:**2.0.0 之后

**时间复杂度:**只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数。

**返回值:**插入后 list 的长度。

示例:


rpush

将一个或者多个元素从右侧放入(尾插)到 list 中。

**语法:**rpush key element [element ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数。

**返回值:**插入后 list 的长度。

示例:


rpushx

在 key 存在时,将一个或者多个元素从右侧放入(尾插)到 list 中。

**语法:**rpushx key element [element ...]

**命令有效版本:**2.0.0 之后

**时间复杂度:**只插入一个元素为 O(1),插入多个元素为 O(N),N 为插入元素个数。

**返回值:**插入后 list 的长度。

示例:


lrange

获取从 start 到 end 区间的所有元素,左闭右闭(闭区间),下标支持负数。

lrange 指的是:list range

**语法:**LRANGE key start stop

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N)

**返回值:**指定区间的元素。

示例:

Redis 的做法是直接尽可能的获取到给定区间范围内的元素,如果给定区间非法,比如超出下标,就会尽可能的获取对应的内容。


lpop

从 list 左侧取出元素(即头删)。

**语法:**lpop key

Redis 5 版本中在这后面是没有 [count] 参数的,从 Redis 6.2 版本开始,新增了一个 count 参数,用来描述此次要删除几个元素。

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**取出的元素或者 nil。

示例:


rpop

从 list 右侧取出元素(即尾删)。

**语法:**rpop key

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**取出的元素或者 nil。

示例:

  • 搭配使用 rpush 和 lpop 就相当于队列。
  • 搭配使用 rpush 和 rpop 就相当于栈。

lindex

获取从左数第 index 位置的元素。

lindex 指的是:list index

**语法:**lindex key index

**命令有效版本:**1.0.0 之后

时间复杂度:O(N)

**返回值:**取出的元素或者 nil。

示例:


linsert

在特定位置插入元素。

**语法:**linsert key <before | after> pivot element

**命令有效版本:**2.2.0 之后

**时间复杂度:**O(N),N 表示列表长度。

**返回值:**插入后的 list 长度。

示例:

insert 进行插入时,要根据基准值找到对应的位置,从左往右找,找到第一个符合基准值的位置即可。如有两个4:


llen

获取 list 长度。

**语法:**llen key

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**list 的长度。

示例:


lrem

根据参数 count 的值,移除列表中与参数 element 相等的元素。

  • count > 0 : 从表头开始向表尾搜索,移除与 element 相等的元素,数量为 count。
  • count < 0 : 从表尾开始向表头搜索,移除与 element 相等的元素,数量为 count的绝对值。
  • count = 0 : 移除表中所有与 element 相等的值。

**语法:**lrem key count element

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N)

**返回值:**被移除元素的数量。 列表不存在时返回 0 。

示例:


ltrim

Redis 的 Ltrim 对一个列表进行修剪(trim),也就是说,让列表只保留 start 和 stop 区间内(闭区间)的元素,不在区间之内的元素都将被直接删除。

**语法:**ltrim key start stop

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N)

**返回值:**命令执行成功时,返回 OK。

示例:


lset

通过索引来设置元素的值。当索引参数超出范围,或对一个空列表进行 LSET 时,返回一个错误。

**语法:**lset key index element

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N)

**返回值:**操作成功返回 OK,否则返回错误信息。

示例:

  • lindex 可以很好的处理下标越界的情况,直接返回 nil。
  • lset 则会报错,不会像 js 一样,直接在 10 这个下标搞出一个元素。

1.2 阻塞版本命令

blpopbrpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本一致,除了:

  • 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会直接返回 nil,但阻塞版本会根据 timeout 阻塞⼀段时间(使用 blpop 和 brpop 时,这里是可以显示设置阻塞时间的,不一定是无休止的等待),期间 Redis 可以执行其他命令(此处的 blpop 和 brpop 看起来好像耗时很长,但实际上并不会对 Redis 服务器产生负面影响),但要求执行该命令的客户端会表现为阻塞状态(如下图所示)。
  • 命令中如果设置了多个键(key),那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
  • 如果多个客户端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。

阻塞版本的 blpop 和非阻塞版本 lpop 的区别:


blpop

lpop 的阻塞版本。

**语法:**blpop key [key ...] timeout

此处还可以指定超时时间,单位是秒(Redis 6 中,超时时间允许设定成小数,Redis 5 得是整数)。

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**取出的元素或者 nil。

示例:


brpop

rpop 的阻塞版本。

效果和 brpop 类似,只不过这里是头删。

**语法:**brpop key [key ...] timeout

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**取出的元素或者 nil。

blpop 和 brpop 这两个阻塞命令的用途主要就是用来作为 "消息队列"。虽然这两个命令可以在一定程度上满足 "消息队列" 这样的需求,但整体来说,功能还是比较有限。


1.3 List命令总结和内部编码

下表是List命令的作用和时间复杂度:


内部编码

列表类型的内部编码有两种(旧版本,现在已经不再使用,了解即可):

  • **ziplist(压缩列表):**当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用 ziplist 来作为列表的内部编码实现来减少内存消耗。
  • **linkedlist(链表):**当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。

现在采用的内部编码都是 quicklist。quicklist 相当于是链表和压缩列表的结合,整体还是一个链表,链表的每个节点是一个压缩列表。每个压缩列表都不让它太大,同时再把多个压缩列表通过链式结构连起来。


1.4 List典型使用场景

1.4.1 消息队列

如下图所示,Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中 "争抢" 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。

阻塞消息队列模型:

brpop 是阻塞操作,当列表为空时,brpop 就会阻塞等待,一直等到其他客户端 push 了元素为止。当新元素到达之后,首先是第一个消费者拿到元素(按照执行 brpop 命令的先后顺序来决定是谁获取到)。第一个消费者拿到元素之后,也就从 brpop 中返回了(相当于这个命令执行完了)。如果第一个消费者还想继续消费,就需要重新执行 brpop,排在最后。此时,再来一个新的元素过来,就是第二个消费者拿到该元素,以此类推。


1.4.2 分频道的消息队列

如下图所示,Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。

Redis 分频道阻塞消息队列模型:

多个列表(channel)/ 频道(topic),这种场景很常见,日常使用的一些程序,比如抖音。有一个通道用来传输短视频数据,还可以有一个通道来传输弹幕,一个通道来传输点赞、转发、收藏数据,一个通道来传输评论数据......弄成多个频道就可以在某种数据发生问题时,不会对其他数据造成影响(解耦合)。


1.4.3 微博 Timeline

每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

1. 每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content

java 复制代码
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx

2. 向用户 Timeline 添加微博,user:<uid>:mblogs 作为微博的键

java 复制代码
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9

3. 分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博

java 复制代码
keylist = lrange user:1:mblogs 0 9
for key in keylist {
    hgetall key
}

此方案在实际中可能存在两个问题:

1 + n 问题。即如果每次分页获取的微博个数较多(不确定当前一页中有多少数据,可能会导致下面的循环次数很多),需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线 / 管道)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。虽然这里是多个 Redis 命令,但是把这些命令合并成一个网络请求进行通信,这样就大大降低了客户端和服务器之间的交互次数了。

分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分

选择列表类型时,请参考:

  • 同侧存取(lpush + lpop 或者 rpush + rpop)为栈。
  • 异侧存取(lpush + rpop 或者 rpush + lpop)为队列。

2. Set 集合

集合类型也是保存多个字符串类型的元素的(可以使用 json 格式让 string 也能存储结构化数据),但和列表类型不同的是,集合中:

  1. 元素之间是无序的。(此处的 "无序" 是和 list 的有序相对应的)
  2. 元素不允许重复,如下图所示。

集合类型:

一个集合中最多可以存储个元素。Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。

  • list:[1, 2, 3] 和 [2, 1, 3] 是两个不同的 list。
  • set:[1, 2, 3] 和 [2, 1, 3] 是同一个集合。

2.1 Set 集合常见命令

sadd

将一个或者多个元素添加到 set 中。

注意:重复的元素无法添加到 set 中。

**语法:**sadd key member [member ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**本次添加成功的元素个数。

示例:


smembers

获取一个 set 中的所有元素,注意,元素间的顺序是无序的。

**语法:**smembers key

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 是集合中的元素个数。

**返回值:**所有元素的列表。

示例:


sismember

判断一个元素在不在 set 中。

**语法:**sismember key member

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**1 表示元素在 set 中。0 表示元素不在 set 中或者 key 不存在。

示例:


scard

获取一个 set 的基数(cardinality),即 set 中的元素个数。

**语法:**scard key

命令有效版本:1.0.0 之后

**时间复杂度:**O(1)

**返回值:**set 内的元素个数。


spop

从 set 中删除并返回⼀个或者多个元素。

注意 :由于 set 内的元素是无序的,所以取出哪个元素实际是未定义行为,即可以看作随机的。

**语法:**spop key [count]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 是 count

**返回值:**取出的元素。

示例:


smove

将一个元素从源 set 取出并放入目标 set 中。

**语法:**smove source destination member

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(1)

**返回值:**1 表示移动成功,0 表示失败。

示例:

针对上述情况,smove 不会视为出错,也会按照删除、插入来执行。


srem

将指定的元素从 set 中删除。

**语法:**srem key member [member ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 是要删除的元素个数.

**返回值:**本次操作删除的元素个数。

示例:


2.2 Set 集合间命令

交集(inter)、并集(union)、差集(diff)的概念和数学一样,如下图所示:

集合求交集、并集、差集:

sinter

获取给定 set 的交集中的元素。

**语法:**sinter key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。

**返回值:**交集的元素。

示例:


sinterstore

获取给定 set 的交集中的元素并保存到目标 set 中。

要想知道交集的内容,直接按照集合的方式访问目标 set 这个 key 即可。

**语法:**sinterstore destination key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。

**返回值:**交集的元素个数。

示例:


sunion

获取给定 set 的并集中的元素。

**语法:**sunion key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 给定的所有集合的总的元素个数。

**返回值:**并集的元素。

示例:


sunionstore

获取给定 set 的并集中的元素并保存到目标 set 中。

**语法:**sunionstore destination key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 给定的所有集合的总的元素个数。

**返回值:**并集的元素个数。

示例:


sdiff

获取给定 set 的差集中的元素。

**语法:**sdiff key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 给定的所有集合的总的元素个数。

**返回值:**差集的元素。

示例:


sdiffstore

获取给定 set 的差集中的元素并保存到⽬标 set 中。

**语法:**sdiffstore destination key [key ...]

**命令有效版本:**1.0.0 之后

**时间复杂度:**O(N),N 给定的所有集合的总的元素个数.

**返回值:**差集的元素个数。

示例:


2.3 Set命令小结和内部编码

下表总结了集合类型的常见命令:

Set 集合类型命令:


集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使⽤。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。

1. 当元素个数较少并且都为整数时,内部编码为 intset

2. 当元素个数超过 512 个,内部编码为 hashtable。

3. 当存在元素不是整数时,内部编码为 hashtable。


2.4 Set集合使用场景

场景一:集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。 例如一个电子商务网站会对不同标签的用户做不同的产品推荐。

下面的演示通过集合类型来实现标签的若干功能。

1. 给用户添加标签:

java 复制代码
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4

2. 给标签添加用户:

java 复制代码
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:4 user:9 user:28

3. 删除用户下的标签:

java 复制代码
srem user:1:tags tag1 tag5
...

4. 删除标签下的用户:

java 复制代码
srem tag1:users user:1
srem tag5:users user:1
...

场景二:还可以使用 Set 来计算用户之间的共同好友(基于 "集合求交集"),基于此还可以做一些好友推荐。

**场景三:使用 Set 还能统计 UV(去重)。一个互联网产品如何衡量用户量,用户规模呢?**主要的指标是以下两个方面:

  1. PV(Page View),用户每次访问该服务器都会产生一个 pv。
  2. UV(User View),每个用户访问服务器都会产生一个 uv,但是同一个用户多次访问并不会使 uv 增加。uv 需要按照用户进行去重,去重的过程就可以使用 Set 来实现。

本篇完。

下一篇:Redis存储⑥Redis五大数据类型之Zset+渐进式遍历+数据库管理。

相关推荐
利瑞华28 分钟前
Redis 深度解析 —— 高频面试题与核心知识点
数据库·redis·缓存
vip1024p1 小时前
【玩转全栈】----Django基本配置和介绍
数据库·django·sqlite
吴声子夜歌1 小时前
Linux运维——文件内容查看编辑
java·linux·运维
非凡的世界2 小时前
数据结构在 Web 开发中的重要性与应用
数据库·php·编程语言
GottdesKrieges2 小时前
GaussDB用户权限管理
数据库·oracle·gaussdb
m0_748237153 小时前
Linux(CentOS)安装 MySQL
linux·mysql·centos
小镇敲码人3 小时前
【Linux网络编程】之配置阿里云安全组
linux·网络·阿里云
Golinie3 小时前
【Linux网络编程】谈谈网络编程中的select、poll、epoll、Reactor、Proactor模型(下)
linux·网络·reactor·epoll·io多路复用
飞翔的煤气罐boom3 小时前
TCP服务器与客户端搭建
linux·tcp/ip·c
小林熬夜学编程3 小时前
【MySQL】第二弹---数据库基础全解析:从概念到实践的深度探索
linux·开发语言·数据库·mysql·算法