Redis常用数据结构及应用场景
1. 概述
Redis 一个开源的基于键值对(Key-Value)NoSQL 数据库。使用 ANSIC 语言编写、支持网络、基于内存但支持持久化。性能优秀,并提供多种语言的 API。
我们要首先理解一点,我们把 Redis 称为 KV 数据库,键值对数据库,那就可以把 Redis 内部的存储视为存在着一个巨大的 Map,对 Map 的操作无非就是get 和 put,然后通过 key 操作这个 key 所对应的 value,而这个 value 的类型可以多种多样,也就是 Redis 为我们提供的那些数据结构,比如字符串(String)、哈希(Hash)等等。
Redis 会将所有数据都存放在内存中,所以它的读写性能非常惊人。不仅如此,Redis 还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会丢失。
除了上述功能以外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等附加功能。
1.1 应用场景
缓存
缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis 提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,一个合理的缓存设计能够为一个网站的稳定保驾护航。
排行榜系统
排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis 提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
计数器应用
计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据的实时性,每一次播放和浏览都要做+1
的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
消息队列系统
消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务解耦、 非实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足。(我上家公司就用过 Redis 做消息队列,虽然后面换了其他 MQ)。
1.2 特性
速度快
正常情况下,Redis 执行命令的速度非常快,官方给出的数字是读写性能可以达到 10 万/秒。
基于键值对的数据结构服务器
几乎所有的编程语言都提供了类似字典的功能,例如 Java 里的 map,类似于这种组织数据的方式叫作基于键值的方式,与很多键值对数据库不同的是,Redis 中的值不仅可以是字符串,而且还可以是具体的数据结构,这样不仅能便于在许多应用场景的开发,同时也能够提高开发效率。
Redis 的全称是 Remote Dictionary Server,它主要提供了 5 种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础之上演变出了位图(Bitmaps)和 HyperLogLog 两种数据结构,并且随着 LBS (Location BasedService,基于位置服务)的不断发展,Redis 中加入有关 GEO(地理信息定位)的功能。
丰富的功能
除了 5 种数据结构,Redis 还提供了许多额外的功能:提供了键过期功能,可以用来实现缓存。
提供了发布订阅功能,可以用来实现消息系统。支持 Lua 脚本功能,可以利用 Lua 创造出新的 Redis 命令。提供了简单的事务功能,能在一定程度上保证事务特性。提供了流水线(Pipeline)功能,这样客户端能将一批命令一次性传到 Redis,减少了网络的开销。
简单稳定
Redis 的简单主要表现在三个方面。
首先,Redis 的源码很少,早期版本的代码只有 2 万行左右,3.0 版本以后由于添加了集群特性,代码增至 5 万行左右。
其次,Redis 使用单线程模型,这样不仅使得 Redis 服务端处理模型变得简单,而且也使得客户端开发变得简单。
最后,Redis 不需要依赖于操作系统中的类库。
Redis 虽然很简单,但是不代表它不稳定。实际的运行中很少出现因为 Redis 自身 bug 而宕掉的情况。
客户端语言多
Redis 提供了简单的 TCP 通信协议,很多编程语言可以很方便地接人到 Redis。
持久化
通常看,将数据放在内存中是不安全的,一旦发生断电或者机器故障,重要的数据可能就会丢失,因此Redis提供了两种持久化方式:RDB 和 AOF,即可以用两种策略将内存的数据保存到硬盘中,这样就保证了数据的可持久性。
主从复制
Redis 提供了复制功能,实现了多个相同数据的 Redis 副本,复制功能是分布式Redis 的基础。
高可用和分布式
Redis Sentinel,它能够保证 Redis 节点的故障发现和故障自动转移。Redis 从 3.0 版本正式提供了分布式实现 Redis Cluster,它是 Redis 真正的分布式实现,提供了高可用、读写和容量的扩展性。
2. 下载安装
目前演示的安装操作是基于 Centos7 下讲解。
官方下载地址:https://redis.io/download
# 先新建一个目录
mkdir /usr/local/redis
cd /usr/local/redis
# 下载
wget https://download.redis.io/releases/redis-6.2.6.tar.gz
# 解压
tar -xzvf redis-6.2.6.tar.gz
# 编译
cd redis-6.2.6
make
2.1 启动
Redis 有三种方法启动 Redis:默认配置、带参数启动、配置文件启动。
1️⃣ 默认配置
进入安装好的 Redis 的 src
目录下执行以下命令:
./redis-server
可以看到直接使用 redis-server 启动 Redis 后,会打印出一些日志,通过日志 可以看到一些信息:
当前的 Redis 版本的是 64 位的 6.2.6,默认端口是 6379。Redis 建议使用配置文件来启动,所以这种方式是不会在生产环境中使用的。
2️⃣ 参数启动
redis-server 加上要修改配置名和值(可以是多对),没有设置的配置将使用默认配置,例如:如果要用 6380 作为端口启动 Redis,那么可以执行:
./redis-server --port 6380
不过这种方式一般也用得比较少。
3️⃣ 配置文件启动
将配置写到指定文件里,并启动,主要修改的是安装目录下的 redis.conf
文件。
./redis-server ../redis.conf
2.2 操作
Redis 服务启动完成后,就可以使用 redis-cli 连接和操作 Redis 服务。
2.3 停止
Redis 提供了 shutdown 命令来停止 Redis 服务,例如我们目前已经启动的 Redis 服务,可以执行:
./redis-cli -p 6379 shutdown
Redis 服务端将会显示:
2853:M 15 Dec 2021 20:41:26.593 # User requested shutdown...
2853:M 15 Dec 2021 20:41:26.593 * Saving the final RDB snapshot before exiting.
2853:M 15 Dec 2021 20:41:26.594 * DB saved on disk
2853:M 15 Dec 2021 20:41:26.594 * Removing the pid file.
2853:M 15 Dec 2021 20:41:26.594 # Redis is now ready to exit, bye bye...
除了可以通过 shutdown 命令关闭 Redis 服务以外,还可以通过 kill 进程号的方式关闭掉 Redis,但是强烈不建议使用 kill -9
强制杀死 Redis 服务,不但不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造成 AOF 和复制丢失数据的情况。
shutdown 还有一个参数,代表是否在关闭 Redis 前,生成持久化文件:
./redis-cli -p 6379 shutdown nosave/save
默认是 save,生成持久化文件,如果是 nosave 则不生成持久化文件。
3. 全局命令
在了解 Redis 的数据结构之前,先了解 Redis 的一些全局命令。
命令 | 说明 |
---|---|
keys * | 查看所有键,同时也支持通配符,如 keys n* |
dbsize | 返回当前数据库中键的总数 |
exists | 检查键是否存在,存在返回 1,不存在返回 0,如 exists name |
del | 删除键,无论值是什么数据结构类型,del 命令都可以将其删除。返回删除键个数,删除不存在键返回 0。同时 del 命令可以支持删除多个键,如 del name age |
expire | Redis 支持对键添加过期时间,当超过过期时间后,会自动删除键,时间单位秒,如 expire name 10 |
ttl | ttl 命令会返回键的剩余过期时间,若返回 -1 则表示键没设置过期时间,-2 键不存在 |
type | 返回键的数据结构类型 |
randomkey | 随机返回一个键 |
rename | 键重命名,为了防止被强行 rename,Redis 提供了 renamenx 命令,确保只有 newKey 不存在时候才被覆盖。由于重命名键期间会执行 del 命令删除旧的键,如果键对应的值比较大,会存在阻塞 Redis 的可能性 |
注:
- dbsize 命令在计算键总数时不会遍历所有键,而是直接获取 Redis 内置的键总数变量,所以 dbsize 命令的时间复杂度是 O(1)。而 keys 命令会遍历所有键,所以它的时间复杂度是 o(n),当 Redis 保存了大量键时线上环境禁止使用 keys 命令;
- 除了 expire、ttl 命令以外,Redis 还提供了 expireat、pexpire,pexpireat、pttl、persist 等一系列命令,可自行查验。
4. 基本数据结构
Redis 提供了一些数据结构供我们往 Redis 中存取数据,最常用的的有 5 种,字符串(String)、哈希(Hash)、列表(list)、集合(set)、有序集合(ZSET)。
4.1 String
字符串类型是 Redis 最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。字符串类型的值实际可以是字符串(简单的字符串、复 杂的字符串(例如 JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过 512 MB。
1️⃣ 常用命令
设置值 set
set key value [ex seconds] [px milliseconds] [nxlxx]
ex seconds
:为键设置秒级过期时间。px milliseconds
:为键设置毫秒级过期时间。nx
:键必须不存在,才可以设置成功,用于添加。xx
:与 nx 相反,键必须存在,才可以设置成功,用于更新。
其中,ex 参数和 expire 命令基本一样。还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。
除了 set 选项,Redis 还提供了 setex 和 setnx 两个命令:
setex key seconds value
setnx key value
setex 和 setnx 的作用和 ex 和 nx 选项是一样的。也就是,setex 为键设置秒级过期时间,setnx 设置时键必须不存在,才可以设置成功。
有什么应用场景吗?
以 setnx 命令为例子,由于 Redis 的单线程命令处理机制,如果有多个客户端同时执行 setnx key value,根据 setnx 的特性只有一个客户端能设置成功,setnx 可以作为分布式锁的一种实现方案。
获取值 get
get key
如果要获取的键不存在,则返回 nil
。
另外,除了单个设置和获取键值,Redis 还支持批量操作。
批量设置值 mset
mset name ayue age 20 sex 男
批量获取值 mget
mget name age sex
如果有些键不存在,那么它的值为 nil
,结果是按照传入键的顺序返回。
批量操作命令可以有效提高效率,假如没有 mget 这样的命令,要执行 n 次 get 命令具体耗时如下:
n 次 get 时间 = n 次网络时间 + n 次命令时间
使用 mget 命令后,要执行 n 次 get 命令操作具体耗时如下:
n 次 get 时间 = 1 次网络时间 + n 次命令时间
Redis 可以支撑每秒数万的读写操作,但是这指的是 Redis 服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,假设网络时间为 1 毫秒,命令时间为 0.1 毫秒(按照每秒处理 1 万条命令算),那么执行 1000 次 get 命令需要 1.1 秒(1000*1+1000*0.1=1100ms
),1 次 mget 命令的需要 0.101 秒 (1*1+1000*0.1=101ms
)。
数字运算 incr
incr 命令用于对值做自增操作,返回结果分为三种情况:
- 值不是整数,返回错误;
- 值是整数,返回自增后的结果;
- 键不存在,按照值为 0 自增,返回结果为 1。
incr key
除了 incr 命令,Redis 提供了 decr(自减)、 incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数)。
追加指令 append
append 可以向字符串尾部追加值。
append key value
strlen
返回字符串长度。
strlen key
截取字符串 getrange
getrange 截取字符串中的一部分,形成一个子串,需要指明开始和结束的偏移量,截取的范围是个闭区间。
命令 | 说明 | 时间复杂度 |
---|---|---|
get key | 获取值 | O(1) |
del key [key …] | 删除key | O(N)(N是键的个数) |
mset key [key value …] | 批量设置值 | O(N)(N是键的个数) |
mget key [key …] | 批量获取值 | O(N)(N是键的个数) |
incr key | 将 key 中储存的数字值增一 | O(1) |
decr key | 将 key 中储存的数字值减一 | O(1) |
incrby key increment | 将 key 所储存的值加上给定的增量值(increment) | O(1) |
decrby key increment | key 所储存的值减去给定的减量值(decrement) | O(1) |
incrbyfloat key increment | 将 key 所储存的值加上给定的浮点增量值(increment) | O(1) |
append key value | 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾 | O(1) |
strlen key | 返回 key 所储存的字符串值的长度。 | O(1) |
setrange key offset value | 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始 | O(1) |
getrange key start end | 返回 key 中字符串值的子字符 | O(N)(N是字符串的长度) |
2️⃣ 命令的时间复杂度
字符串这些命令中,除了 del 、mset、 mget 支持多个键的批量操作,时间复杂度和键的个数相关,为 O(n),getrange 和字符串长度相关,也是 O(n),其余的命令基本上都是 O(1)的时间复杂度,在速度上是非常快的。
3️⃣ 使用场景
字符串类型的使用场景很广泛,如下:
1、缓存功能
Redis 作为缓存层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低 后端压力的作用。
2、计数
使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
3、共享 Session
一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各 自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题, 可以使用 Redis 将用户的 Session 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或者查询登录信息都直接从 Redis 中集中获取。
4、限时
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。一些网站限制一个 IP 地址不能在一秒钟之内访问超过 n 次。或者同一 IP 在短时间内多次浏览谋篇文章浏览次数不会一直增加。点赞次数在短时间内不能重复点赞。
4.2 Hash
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 – 1 键值对(40多亿)。
1️⃣ 常用命令
基本上,哈希的操作命令和字符串的操作命令很类似,很多命令在字符串类型的命令前面加上了 h 字母,代表是操作哈希类型,同时还要指明要操作的 field 的值。
hset
hset key field value
如果设置成功会返回 1,反之会返回 0。此外 Redis 提供了 hsetnx 命令,它们的关系就像 set 和 setnx 命令一样,只不过作用域由键变为 field。
127.0.0.1:6379> hset hash:test name ayue
(integer) 1
127.0.0.1:6379>
hget
hget key field
获取值
127.0.0.1:6379> hget hash:test name
"ayue"
127.0.0.1:6379>
其他命令:
命令 | 说明 | 时间复杂度 |
---|---|---|
HDEL key field [field] | 删除一个或多个Hash的field | O(N) N是被删除的字段数量 |
HEXISTS key field | 判断field是否存在于Hash中 | O(1) |
HGET key field | 获取Hash中field的值 | O(1) |
HGETALL key | 从Hash中读取全部的域和值 | O(N) N是Hash的长度 |
HINCRBY key field increment | 将Hash中指定域的值增加给定的数字 | O(1) |
HINCRBYFLOAT key field increment | 将Hash中指定域的值增加给定的浮点数 | O(1) |
HKEYS key | 获取Hash的所有字段 | O(N) N是Hash的长度 |
HLEN key | 获取Hash里所有字段的数量 | O(1) |
HMGET key field field | 获取Hash里面指定字段的值 | O(N) N是请求的字段数 |
HMSET key field value [field value …] | 批量设置Hash字段值 | O(N) N是设置的字段数 |
HSET key field value | 设置Hash里面一个字段的值 | O(1) |
HSETNX key field value | 设置Hash的一个字段,只有当这个字段不存在时有效 | O(1) |
HSTRLEN key field | 获取Hash里面指定field的长度 | O(1) |
HVALS key | 获得 Hash 的所有值 | O(N) N是Hash的长度 |
HSCAN key cursor [MATCH pattern] [COUNT count] | 迭代 Hash 里面的元素 |
2️⃣ 命令的时间复杂度
哈希类型的操作命令中,hdel,hmget,hmset 的时间复杂度和命令所带的 field 的个数相关 O(k),hkeys,hgetall,hvals 和存储的 field 的总数相关,O(N)。其余的命令时间复杂度都是 O(1)。
3️⃣ 使用场景
1、存储对象
Redis哈希对象常常用来缓存一些对象信息,如用户信息、商品信息、配置信息等。
我们以用户信息为例,它在关系型数据库中的结构是这样的:
id | name | age |
---|---|---|
1 | Tom | 15 |
2 | Jerry | 13 |
而使用Redis Hash存储其结构如下图:
hmset user:1 name Tom age 15
hmset user:2 name Jerry age 13
相比较于使用Redis字符串存储,其有以下几个优缺点:
-
原生字符串每个属性一个键。
set user:1:name Tom set user:1:age 15
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。 -
序列化字符串后,将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。 -
序列化字符串后,将用户信息序列化后用一个键保存
hmset user:1 name Tom age 15
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
2、购物车
购物车主要功能是临时存放欲购买的商品,然后在结算或下订单时,把购物里面的数据全部移除。其数据结构主要包含的字段有:用户ID、商品ID、商品数量等等。通常我们需要实现以下几个功能:
- 全选功能,获取所有该用户的所有购物车商品;
- 商品数量,购物车图标上要显示购物车里商品的总数;
- 删除,要能移除购物车里某个商品;
- 增加或减少某个商品的数量。
在之前很多电商网站通过 cookie 实现购物车功能,也就是将整个购物车都存储到 cookie里面。
- 优点:无须对数据库进行写入就可以实现购物车功能,这种方式大大提高了购物车的性能。
- 缺点:程序需要重新解析和验证( validate) cookie,确保 cookie 的格式正确,并且包含的商品都是真正可购买的商品。另外,因为浏览器每次发送请求都会连 cookie 一起发送,所以如果购物车 cookie 的体积比较大,那么请求发送和处理的速度可能会有所降低。
而通过 Redis 定义购物车非常简单:当前登录用户 ID 号做为key,商品 ID 号为 field,加入购物车数量为 value,如下:
hmset cart:001 prod:01 1 prod:02 1
| | |
| | |
| | |
key field value
而对于上述功能,可以通过 Hash 的相关命令来操作。
4.3 List
列表( list)类型是用来存储多个有序的字符串,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 2-1 个元素。
在 Redis 中,可以对列表两端插入( push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
列表类型有两个特点:
- 列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。
- 列表中的元素可以是重复的。
1️⃣ 常用命令
Redis列表对象常用命令如下表(点击命令可查看命令详细说明):
命令 | 说明 | 时间复杂度 |
---|---|---|
BLPOP key [key …] timeout | 删除,并获得该列表中的第一元素,或阻塞,直到有一个可用 | O(1) |
BRPOP key [key …] timeout | 删除,并获得该列表中的最后一个元素,或阻塞,直到有一个可用 | O(1) |
BRPOPLPUSH source destination timeout | 弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用 | O(1) |
LINDEX key index | 获取一个元素,通过其索引列表 | O(N) |
LINSERT key BEFORE | AFTER pivot value在列表中的另一个元素之前或之后插入一个元素 | O(N) |
LLEN key | 获得队列(List)的长度 | O(1) |
LPOP key | 从队列的左边出队一个元素 | O(1) |
LPUSH key value [value …] | 从队列的左边入队一个或多个元素 | O(1) |
LPUSHX key value | 当队列存在时,从队到左边入队一个元素 | O(1) |
LRANGE key start stop | 从列表中获取指定返回的元素 | O(S+N) |
LREM key count value | 从列表中删除元素 | O(N) |
LSET key index value | 设置队列里面一个元素的值 | O(N) |
LTRIM key start stop | 修剪到指定范围内的清单 | O(N) |
RPOP key | 从队列的右边出队一个元 | O(1) |
RPOPLPUSH source destination | 删除列表中的最后一个元素,将其追加到另一个列表 | O(1) |
RPUSH key value [value …] | 从队列的右边入队一个元素 | O(1) |
RPUSHX key value | 从队列的右边入队一个元素,仅队列存在时有效 | O(1) |
2️⃣ 命令的时间复杂度
列表类型的操作命令中,llen,lpop,rpop,blpop 和 brpop 命令时间复杂度都是 O(1),其余的命令的时间复杂度都是 O(n),只不过 n 的值根据命令不同而不同,比如 lset,lindex 时间复杂度和命令后的索引值大小相关,rpush 和 lpush 和插入元素的个数相关等等。
3️⃣ 使用场景
1、消息队列
但使用 Redis 做消息队列存在很多问题,如消息确认 ACK,消息丢失等,所以一般来说还是用比较专业的 MQ 中间件。
2、文章列表
如下面这样的文章列表,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。
上图可表示为:
# 深圳卫健委发布一条消息,消息ID为 99
lpush mes:001 99
# 武汉本地宝发布一条消息,消息ID为 100
lpush mes:001 100
# 获取消息列表‘
lrange mes:001 0 5
4.4 Set
集合( set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
一个集合最多可以存储 232 – 1 个元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
1️⃣ 常用命令
Redis Set 对象常用命令如下表(点击命令可查看命令详细说明):
命令 | 说明 | 时间复杂度 |
---|---|---|
SADD key member [member …] | 添加一个或者多个元素到集合(set)里 | O(N) |
SCARD key | 获取集合里面的元素数量 | O(1) |
SDIFF key [key …] | 获得队列不存在的元素 | O(N) |
SDIFFSTORE destination key [key …] | 获得队列不存在的元素,并存储在一个关键的结果集 | O(N) |
SINTER key [key …] | 获得两个集合的交集 | O(N*M) |
SINTERSTORE destination key [key …] | 获得两个集合的交集,并存储在一个关键的结果集 | O(N*M) |
SISMEMBER key member | 确定一个给定的值是一个集合的成员 | O(1) |
SMEMBERS key | 获取集合里面的所有元素 | O(N) |
SMOVE source destination member | 移动集合里面的一个元素到另一个集合 | O(1) |
SPOP key [count] | 删除并获取一个集合里面的元素 | O(1) |
SRANDMEMBER key [count] | 从集合里面随机获取一个元素 | |
SREM key member [member …] | 从集合里删除一个或多个元素 | O(N) |
SUNION key [key …] | 添加多个set元素 | O(N) |
SUNIONSTORE destination key [key …] | 合并set元素,并将结果存入新的set里面 | O(N) |
[SSCAN key cursor MATCH pattern] [COUNT count] | 迭代set里面的元素 | O(1) |
2️⃣ 命令的时间复杂度
scard,sismember 时间复杂度为 O(1),其余的命令时间复杂度为 O(n),其中 sadd,srem 和命令后所带的元素个数相关,spop,srandmember 和命令后所带 count 值相关,交集运算 O(m*k),k 是多个集合中元素最少的个数,m 是键个数,并集、差集和所有集合的元素个数和相关。
3️⃣ 使用场景
1、抽奖活动
常见的抽奖活动,比如基于 Redis 实现抽奖功能。
SPOP(随机移除并返回集合中一个或多个元素) 和 SRANDMEMBER(随机返回集合中一个或多个元素) 命令可以帮助我们实现一个抽奖系统,如果允许重复中奖,可以使用SRANDMEMBER 命令。
活动 ID 为 001,则
# Tom userID:01 参加活动
sadd action:001 01
# Jerry userID:02 参加活动
sadd action:001 02
# 开始抽奖1名中奖者
srandmember action:001 1 或 spop action:001 1
# 查看有多少用户参加了本次抽奖
smembers action:001
2、点赞功能
比如设计一个微信点赞功能。
# 张三用户ID 为userId:01
# 张三对消息 ID008点赞啦
sadd zan:008 userId:01
# 张三取消了对消息008的点赞
srem zan:008 userId:01
# 检查用户是否点过赞
sismember zan:008 userId:01
# 获取消息ID008所有的点赞用户列表
smembers zan:008
# 消息ID008的点赞数计算
scard zan:008
3、关系设计
如我们要设计一个微博的共同关注,或者可能认识的人。设计如下:
① A 关注的人
sadd A:cares B C D E
② B 关注的人
sadd B:cares A C D F
③ C 关注的人
sadd C:cares A F
按照以上条件:
④ A 和 B 共同关注的人
# D,C
sinter A:cares B:cares
⑤ 我关注的人也关注他
# A 关注的 B 也关注了 F,返回 1 否则返回 0
sismember B:cares F
⑥ 可能认识的人
# C 可能认识的人 C,D
sdiff B:cares C:cares
4、集合操作
setA={A,B,C} setB={B, C}
① 集合与集合之间的交集
sinter setA setB-->得到集合{B,C}
② 集合与集合之间的并集
sunion setA setB -->得到集合{A,B,C}
③ 集合与集合之间的差集
sdiff setA setB-->得到集合{A}
127.0.0.1:6379> SADD setA A B C
(integer) 3
127.0.0.1:6379> SADD setB B C
(integer) 2
127.0.0.1:6379> SINTER setA setB
1) "C"
2) "B"
127.0.0.1:6379> SUNION setA setB
1) "A"
2) "B"
3) "C"
127.0.0.1:6379> SDIFF setA setB
1) "A"
127.0.0.1:6379>
4.5 ZSet
ZSet,有序集合,相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数( score)作为排序的依据。
有序集合中的元素不能重复,但是 score 可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。
有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
1️⃣ 常用命令
zadd
向有序集合 top:20211221
添加话题和点击量。
zadd hot:20211220 10 薇娅逃税
zadd 命令还有四个选项 nx、xx、ch、incr 四个选项:
- nx,member 必须不存在,才可以设置成功,用于添加;
- xx,member 必须存在,才可以设置成功,用于更新;
- ch,返回此次操作后,有序集合元素和分数发生变化的个数;
- incr,对 score 做增加,相当于 zincrby 。
Redis列表对象常用命令如下表:
命令 | 说明 | 时间复杂度 |
---|---|---|
BZPOPMAX key [key …] timeout | 从一个或多个排序集中删除并返回得分最高的成员,或阻塞,直到其中一个可用为止 | O(log(N)) |
BZPOPMIN key [key …] timeout | 从一个或多个排序集中删除并返回得分最低的成员,或阻塞,直到其中一个可用为止 | O(log(N)) |
[ZADD key NXXX] [CH] [INCR] score member [score member …] | 添加到有序set的一个或多个成员,或更新的分数,如果它已经存在 | O(log(N)) |
ZCARD key | 获取一个排序的集合中的成员数量 | O(1) |
ZCOUNT key min max | 返回分数范围内的成员数量 | O(log(N)) |
ZINCRBY key increment member | 增量的一名成员在排序设置的评分 | O(log(N)) |
ZINTERSTORE | 相交多个排序集,导致排序的设置存储在一个新的关键 | O(NK)+O(Mlog(M)) |
ZLEXCOUNT key min max | 返回成员之间的成员数量 | O(log(N)) |
ZPOPMAX key [count] | 删除并返回排序集中得分最高的成员 | O(log(N)*M) |
ZPOPMIN key [count] | 删除并返回排序集中得分最低的成员 | O(log(N)*M) |
ZRANGE key start stop [WITHSCORES] | 根据指定的index返回,返回sorted set的成员列表 | O(log(N)+M) |
ZRANGEBYLEX key min max [LIMIT offset count] | 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。 | O(log(N)+M) |
ZREVRANGEBYLEX key max min [LIMIT offset count] | 返回指定成员区间内的成员,按字典倒序排列,分数必须相同 | O(log(N)+M) |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] | 返回有序集合中指定分数区间内的成员,分数由低到高排序。 | O(log(N)+M) |
ZRANK key member | 确定在排序集合成员的索引 | O(log(N)) |
ZREM key member [member …] | 从排序的集合中删除一个或多个成员 | O(M*log(N)) |
ZREMRANGEBYLEX key min max | 删除名称按字典由低到高排序成员之间所有成员。 | O(log(N)+M) |
ZREMRANGEBYRANK key start stop | 在排序设置的所有成员在给定的索引中删除 | O(log(N)+M) |
ZREMRANGEBYSCORE key min max | 删除一个排序的设置在给定的分数所有成员 | O(log(N)+M) |
ZREVRANGE key start stop [WITHSCORES] | 在排序的设置返回的成员范围,通过索引,下令从分数高到低 | O(log(N)+M) |
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] | 返回有序集合中指定分数区间内的成员,分数由高到低排序。 | O(log(N)+M) |
ZREVRANK key member | 确定指数在排序集的成员,下令从分数高到低 | O(log(N)) |
ZSCORE key member | 获取成员在排序设置相关的比分 | O(1) |
ZUNIONSTORE | 添加多个排序集和导致排序的设置存储在一个新的键 | O(N)+O(M log(M)) |
ZSCAN key cursor [MATCH pattern] [COUNT count] | 迭代sorted sets里面的元素 | O(1) |
2️⃣ 命令的时间复杂度
参考上表。
3️⃣ 使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
如上热搜榜,以日期为 key :
① 点击热搜,每次加 1
zincrby hot:20211220 1 薇娅逃税
② 右侧排行实现,展示今日前 50 排名
# zrange 是从低到高返回,zrevrange 反之
zrevrange hot:20211221 0 49 withscores