Redis(三)常用的五种数据结构

150 阅读16分钟

Redis提供了一些数据结构供我们存取数据,最常用的的有5种,字符串(String)、哈希(Hash)、列表(list)、集合(set)、有序集合(ZSET)。

字符串(String)

字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

(虽然Redis是C写的,C里面有字符串<本质使用char数组来实现>,但是处于种种考虑,Redis还是自己实现了字符串类型)

操作命令

set key value

set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|

set命令有几个选项:

  • 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设置时键必须不存在,才可以设置成功。

应用场景

由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案。当然分布式锁没有不是只有一个命令就OK了,其中还有很多的东西要注意,我们后面会用单独的章节来讲述基于Redis的分布式锁。

get key

如果要获取的键不存在,则返回nil(空):

mset key value [key value ...]

批量设置值

mset key1 value1 key2 value2

mget key [key ...]

批量获取值,如果有些键不存在,那么它的值为nil(空),结果是按照传入键的顺序返回。

mget key1 key-null key2

截屏2023-06-09 14.13.48.png

批量操作命令可以有效提高效率,假如没有mget这样的命令,要执行n次get命令具体耗时如下:

n次 get时间=n次网络时间+n次命令时间

使用mget命令后,要执行n次get命令操作具体耗时如下:

n次get时间=1次网络时间+n次命令时间

incr key

incr命令用于对值做自增操作,返回结果分为三种情况:

值不是整数,返回错误。

值是整数,返回自增后的结果。

键不存在,按照值为0自增,返回结果为1。

除了incr命令,Redis提供了decr(自减)、 incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数),具体效果可自行尝试。

append key value

append可以向字符串尾部追加值,将本次传入的value添加到key所对应的value后面

strlen key

返回字符串长度(每个中文占3个字节)

getset key value

getset和set一样会设置值,但是不同的是,它同时会返回键原来的值

截屏2023-06-09 14.20.17.png

setrange key offset value

设置指定位置的字符,下标从0开始计算。

截屏2023-06-09 14.22.41.png

getrange key start end

getrange 截取字符串中的一部分,形成一个子串,需要指明开始和结束的偏移量,截取的范围是个闭区间。

截屏2023-06-09 14.23.49.png

命令的时间复杂度

字符串这些命令中,除了del 、mset、 mget支持多个键的批量操作,时间复杂度和键的个数相关,为O(n),getrange和字符串长度相关,也是O(n),其余的命令基本上都是O(1)的时间复杂度,在速度上还是非常快的。

字符串类型的使用场景

缓存功能

Redis 作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

计数

使用Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。

共享Session

一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。

为了解决这个问题,可以使用Redis将用户的Session进行集中管理,,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。

限速

比如,很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。一些网站限制一个IP地址不能在一秒钟之内方问超过n次也可以采用类似的思路。

哈希(Hash)

Java里提供了HashMap,Redis中也有类似的数据结构,就是哈希类型。但是要注意,哈希类型中的映射关系叫作field-value,注意这里的value是指field对应的值,不是键对应的值。

操作命令

哈希的操作命令和字符串的操作命令很类似,很多命令在字符串类型的命令前面加上了h字母,代表是操作哈希类型,同时还要指明要操作的field的值。

hset key field value [field value ...]

截屏2023-06-09 14.27.28.png

如果设置成功会返回成功的个数,反之会返回0。此外Redis提供了hsetnx命令,它们的关系就像set和setnx命令一样,只不过作用域由键变为field。(在redis 4.0.0之前只能设置一个field)

hget key field

如果键或field不存在,会返回nil。 截屏2023-06-09 14.28.42.png

hdel key field [field ...]

hdel会删除一个或多个field,返回结果为成功删除field的个数。

hlen key

hlen会计算并返回field个数

hmset key field value [field value ...]

批量设置field,在redis4.0版本后和hset基本一致

hmget key field [field ...]

批量获取field

截屏2023-06-09 14.36.28.png

hexists key field

判断field是否存在

截屏2023-06-09 14.37.59.png

hkeys key

批量获取key对应的所有的field

截屏2023-06-09 14.39.15.png

hvals key

批量获取key对应的所有的value

截屏2023-06-09 14.41.08.png

hgetall key

在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型,hscan将在后面的章节介绍。

截屏2023-06-09 15.02.11.png

hincrby key field increment

hincrby和 hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作用域是filed。

截屏2023-06-09 15.04.53.png

hstrlen key field

计算field的字符串长度 截屏2023-06-09 15.05.42.png

命令的时间复杂度

哈希类型的操作命令中,hdel,hmget,hmset的时间复杂度和命令所带的field的个数相关O(k),hkeys,hgetall,hvals和存储的field的总数相关,O(N)。其余的命令时间复杂度都是O(1)。

Hash类型使用场景

从前面的操作可以看出,String和Hash的操作非常类似,那为什么要弄一个hash出来存储。

哈希类型比较适宜存放对象类型的数据,我们可以比较下,如果数据库中表记录user为:

idnameage
1kongbai18
2baikong20

1、使用String类型

需要一条条去插入获取。

set user:1:name kongbai;

set user:1:age 18;

set user:2:name baikong;

set user:2:age 20;

优点:简单直观,每个键对应一个值

缺点:键数过多,占用内存多,用户信息过于分散,不用于生产环境

2、将对象序列化存入redis

set user:1 serialize(userInfo);

优点:编程简单,若使用序列化合理内存使用率高

缺点:序列化与反序列化有一定开销,更新属性时需要把userInfo全取出来进行反序列化,更新后再序列化到redis

3、使用hash类型

hmset user:1 name kongbai age 18

hmset user:2 name baikong age 20

优点:简单直观,使用合理可减少内存空间消耗

缺点:要控制内部编码格式,不恰当的格式会消耗更多内存

列表(list)

列表( list)类型是用来存储多个有序的字符串,a、b、c、c、b四个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储(2^32-1)个元素(4294967295)。

image.png

在Redis 中,可以对列表两端插入( push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

列表类型有两个特点:

第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。

第二、列表中的元素可以是重复的。

操作命令

lrange key start stop

获取指定范围内的元素列表(不会删除元素)下标从左到右为0到N-1

lrange 0 -1命令可以从左到右获取列表的所有元素

截屏2023-06-09 15.11.06.png

rpush key element [element ...]

向右插入元素 截屏2023-06-09 15.13.10.png

lpush key element [element ...]

向左插入元素

截屏2023-06-09 15.14.22.png

linsert key BEFORE|AFTER pivot element

在某个元素前或后插入新元素 截屏2023-06-09 15.16.46.png

lpop key [count]

从列表左侧弹出一个或多个元素(弹出时会删除元素)

截屏2023-06-09 15.18.58.png

rpop key [count]

从列表右侧弹出一个或多个元素(弹出时会删除元素) 截屏2023-06-09 15.19.59.png

lrem key count element

lrem命令会从列表中找到等于element的元素进行删除,根据count的不同分为三种情况:

count>0,从左到右,删除最多count个元素。

count<0,从右到左,删除最多count绝对值个元素。

count=0,删除所有。

截屏2023-06-09 15.22.57.png

ltrim key start stop

按照索引范围修剪列表,保留start->stop内的元素

截屏2023-06-09 15.39.11.png

lset key index element

修改指定索引下标的元素 截屏2023-06-09 15.40.33.png

lindex key index

获取列表指定索引下标的元素

截屏2023-06-09 15.42.47.png

llen key

获取列表长度

blpop key [key ...] timeout

blpop是lpop的阻塞版本,除此之外还支持多个列表类型,也支持设定阻塞时间,单位秒,如果阻塞时间为0,表示一直阻塞下去。

blpop后面如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。

brpop key [key ...] timeout

brpop是rpop的阻塞版本,除此之外还支持多个列表类型,也支持设定阻塞时间,单位秒,如果阻塞时间为0,表示一直阻塞下去。

brpop后面如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。

列表使用场景

列表类型可以用于比如:

消息队列,Redis 的 lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

文章列表

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

实现其他数据结构

lpush+lpop = Stack(栈)

lpush +rpop = Queue(队列)

lpsh+ ltrim = Capped Collection(有限集合)

lpush+brpop = Message Queue(消息队列)

集合(set)

image.png

集合( set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。

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

操作命令

集合内操作命令

sadd key member [member ...]

添加元素,允许添加多个,返回结果为添加成功的元素个数 截屏2023-06-09 15.48.38.png

srem key member [member ...]

删除元素,允许删除东哥,返回结果为删除成功的元素个数

截屏2023-06-09 15.50.45.png

scard key

计算元素个数

sismember key member

判断元素是否在集合中,如果给定元素element在集合内返回1,反之返回0

截屏2023-06-09 15.52.58.png

srandmember key [count]

随机从集合返回指定个数元素,指定个数如果不写默认为1,元素不会删除##### 从集合随机弹出元素

截屏2023-06-09 15.53.27.png

spop key [count]

从集合随机弹出元素,同样可以指定个数,如果不写默认为1,注意,既然是弹出,spop命令执行后,元素会从集合中删除,而srandmember不会。

截屏2023-06-09 15.54.26.png

smembers key

获取所有元素(不会弹出元素),无序

截屏2023-06-09 15.56.42.png

集合间操作指令

现在有两个集合,它们分别是set1和set2

截屏2023-06-09 15.59.38.png

sinter key [key ...]

求多个集合的交集

截屏2023-06-09 16.00.40.png

sunion key [key ...]

求多个集合的并集

截屏2023-06-09 16.01.39.png

sdiff key [key ...]

求多个集合的差集

截屏2023-06-09 16.03.15.png

将交集、并集、差集的结果保存
sinterstore destination key [key ...]
suionstore destination key [key ...]
sdiffstore destination key [key ...]

集合间的运算在元素较多的情况下会比较耗时,所以 Redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination key中。

集合使用场景

集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。

例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。

除此之外,集合还可以通过生成随机数进行比如抽奖活动,以及社交图谱等等。

有序集合(ZSET)

image.png

既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。

有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。

有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。

操作命令

集合内操作命令

zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]

添加成员

截屏2023-06-09 16.42.34.png

zadd命令还有四个选项nx、xx、ch、incr 四个选项

  • nx: member必须不存在,才可以设置成功,用于添加。

  • xx: member必须存在,才可以设置成功,用于更新。

  • ch: 返回此次操作后,有序集合元素和分数发生变化的个数

  • incr: 对score做增加,相当于后面介绍的zincrby

zcard key

计算成员个数

zscore key member

计算某个成员的分数,如果成员不存在则返回nil

截屏2023-06-09 16.44.49.png

zrank key member

zrank是从分数从低到高返回排名

zrevrank反之

很明显,排名从0开始计算。

截屏2023-06-09 16.45.40.png

zrem key member [member ...]

删除成员,允许一次删除多个成员,返回结果为成功删除的个数。

zincrby key increment member

增加成员的分数

截屏2023-06-09 16.48.26.png

zrange key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
zrevrange key start stop [WITHSCORES]

有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。如果加上 withscores选项,同时会返回成员的分数

截屏2023-06-09 18.07.35.png

zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
zrevrangebyscore key max min [withscores][limit offset count]

截屏2023-06-09 21.51.40.png 返回指定分数范围的成员 zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。例如下面操作从低到高返回200到221分的成员,withscores选项会同时返回每个成员的分数。

区间的取值使用闭区间 (小于等于或大于等于),你也可以通过给参数前增加 ( 符号来使用可选的开区间 (小于或大于),-inf和+inf分别代表无限小和无限大

截屏2023-06-09 21.55.21.png

zcount key min max

返回指定分数范围成员个数

截屏2023-06-09 21.57.33.png

zremrangebyrank key start end

移除指定排名(rank)区间内的所有成员。

zremrangebyscore key min max

移除指定分数(score)区间内的所有成员。

集合间操作命令

zinterstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

交集

  • destination:交集计算结果保存到这个键。

  • numkeys:需要做交集计算键的个数。

  • key [key ...]:需要做交集计算的键。

  • weights weight [weight ...]:每个键的权重,在做交集计算时,每个键中的每个member 会将自己分数乘以这个权重,每个键的权重默认是1。

  • aggregate sum/ min |max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是sum。

截屏2023-06-09 22.06.26.png

zunionstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

该命令的所有参数和zinterstore是一致的,只不过是做并集计算,大家可以自行实验。

有序集合使用场景

有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。