Redis入门详解之五种基本数据结构

Posted Zheng"Rui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis入门详解之五种基本数据结构相关的知识,希望对你有一定的参考价值。

目录

一、背景

在了解完mysql之后,基本了解了关系型数据库的工作原理,但是,生活中还是存在很多方面,使用mysql很难解决,比如mysql终究读取数据还是要到磁盘上,对于大量的并发,处理效率并不高,很容易成为性能瓶颈,并且mysql结构固定,当确定了表结构之后,往表中添加的数据都是同一个结构,当遇到有多个可选项的时候,性能较低。这些问题都可以通过redis数据库进行解决。

二、redis的五种数据类型

先看redis的五种数据类型以及相应的基本操作。

2.1 字符串类型

2.1.1 基本介绍

字符串类型是redis中最基本的数据类型,与C/C++中的字符串不同的是,redis的字符串可以用来存储任何形式的字符串,包括二进制数据,json化的对象,甚至是一张图片等(了解C/C++的同学应该明白C/C++中的字符串以\\0结尾,不适合存储二进制)。一般规定字符串类型的键,其值可以存放最多512MB的数据。
字符串类型是其他四种类型的基础,从某种角度上来说,其它的数据类型的区别其实就在于如何组织内部的字符串上。

2.1.2 命令

2.1.2.1 赋值和取值

redis中是不区分大小写的,之后我都采用小写
set key value 用来设置key 的值 为 value
get key 用来获取对应key的value,当键不存在的时候,返回nil

2.1.2.2 递增数字

当字符串存储的是数据可以转化成数字的时候,可以使用incr命令来进行数字递增。
set k1 100
incr k1 此时k1变为101
如果key不存在则默认为0,此时递增之后就变为1.
incr操作的key值不是整数的时候,就会报错。
注意,incr命令是原子命令,不会产生竞态关系

2.1.3 实践

2.1.3.1 统计访问量

对于一个博客系统来说,统计每一篇文章的访问量是很常见的功能,这里可以为每一篇文章都建立一个键名为"post:文章ID:page.view"的字符串类型,值为0,每次访问文章的时候,就对该文章对应的键进行incr自增,来保存总的访问量。
提示:在对键进行命名的时候,一个基本的原则就是,让键名有意义,不然很难管理。推荐做法是使用"对象类型:对象ID:对象属性"来命名一个键。对于多个单词之间可以用 . 进行连接。

2.1.3.2 生成自增ID

同样,使用incr可以用来生成自增ID,在mysql中可以设定auto_increment来设置自增字段,在redis中,可以对每一个类型的对象构建一个名为"对象类型:count"的键,每当建立一个该类型的对象的时候,就为该键incr,进行自增,那么此键的值既表示了该类型对象的数量,也能用来作为自增ID。

2.1.3.3 存储对象

同样,字符串类型还可以用来存储json化之后的结构体对象。可以用"对象类型:对象ID"为键,值为json化之后的结构体对象数据,这样存储逻辑简单,比较方便。但同样存在问题,这一点在之后介绍使用哈希存储对象的时候会继续介绍。

2.1.4 命令拾遗

2.1.4.1 incrby 增加指定整数

incr用来对key值进行加1,而使用incrby可以自己指定增加的步长。
incrby k1 10用来对k1的值加10.

2.1.4.2 decr 减少整数

incr用来增加,而decr用来每次对键值减1,同样也有decrby,用来自己设置减少的步长。
decr k1 用来对k1减1
decr k1 10 用来对k1减10

2.1.4.3 incrbyfloat 增加浮点数

incrbyfloat用来对key值增加一个浮点数
incrbyfloat k1 0.2 给k1加0.2

2.1.4.4 append 尾部追加

append用来对字符串进行尾部追加
set k2 hello
append k2 world此时k2为helloworld

2.1.4.5 strlen 获取字符串长度

strlen k2 返回k2的值的长度

2.1.4.6 mset/mget 同时设置/获取多个值

set/get只能设置/获取一个值,可以使用mset/mget来进行批量操作。
mset k3 nihao k4 nihaoya
mget k3 k4就会返回k3,k4的值。

2.2 哈希(散列)类型

2.2.1 基本介绍

之前一节介绍了使用字符串存储json化之后的结构体,当然,这样做确实很方便,但是真的好吗?这里介绍第一个缺点:当我们需要修改结构体中的某个字段的字段值的时候,使用字符串存储,我们只能把整个数据都拿出来,做反序列化,修改,然后序列化,然后将整个数据在存回去,这样做的效率很低。比如在博客系统中,我们将每一个文章的各种信息作为结构体进行存储,当我们想要修改比如文章名,或者文章标签的时候,却需要把整个信息都拿出来,其他的都还好,但是要把整个正文数据都拿出来,效率显而易见很低,尤其是在那种几万字的长文中。
为了解决这个问题,我们可以使用散列类型来对结构体进行存储。
我们知道redis是以键值对的字典形式去存储数据的,而哈希类型的键值也是一个字典,其中保存了字段字段值的映射。其中字段值只能是字符串,这也就意味着在散列类型中不允许类型嵌套,其实在所有的redis类型中,都不允许类型嵌套,列表中的元素也只能是字符串类型,而不能是散列或者集合。
一个散列类型的键最多可以存储 2 32 2^32 232-1 个键值对。
散列类型很适合存储对象,使用对象类型和对象ID作为键名,使用字段作为field,使用字段值作为value。用散列类型存储对象还有一个好处就是对象结构灵活。在mysql中,如果想存储一类对象,就要先设定好表结构,所有对象的表结构都是一样的,这对那些只要是该对象就有的公共字段当然没有问题,但是,万一某个对象想新增一个特有的字段怎么办呢?这时候只能添加列字段,但是这个字段是某个对象特有的,对于其他对象来说,这个字段是冗余的,一个两个还好,如果有很多有特殊字段的对象的话,就有很多冗余了。而redis就没有这个问题,以散列进行存储对象,字段都是每个对象自己按照自己的情况添加的,相对独立。

2.2.2 命令

2.2.2.1 hset/hget || hmset/hmget || hgetall

在散列类型中,使用hset进行赋值
hset key field value 来设置某个key对应的散列的字段和字段值
hget key field hget用来获取某个key中的字段对应的字段值
hset的方便之处在于不区分当前操作是插入操作还是更新操作,只需要使用hset即可,如果之前没有数据,则返回1,如果之前有数据,则会进行更新,然后返回0,甚至,如果之前没有key,也会自动创建key。
在redis中,每个键都有明确的数据类型,hset创建的键类型为散列类型,set创建的键类型为字符串,如果使用一种数据类型的命令去操作另一种数据类型,就会报错。
hmset k1 f1 v1 f2 v2 f3 v3当需要同时赋值多个field的时候,可以使用hmset
hmget k1 f1 f2 f3可以使用hmget同时获取多个field
hgetall k1 hgetall会返回该键对应的所有字段和字段值,并以列表的形式进行返回

2.2.2.2 hexists 判断字段是否存在

hexists k1 f1 用来判断k1中是否有f1字段,如果有返回1,没有则返回0

2.2.2.3 hsetnx 当field不存在的时候才进行赋值

hsetnx k1 f1 v1 当f1不存在的时候才会进行赋值

2.2.2.4 hincrby 增加字段的值

hincrby k1 f1 60将k1中的f1字段值加60,命令会返回增值后的字段值

2.2.2.5 hdel 删除一个或多个字段

hdel k1 f1 f2 f3 hdel可以用来删除key中的一个或多个字段,返回值是删除字段的个数

2.2.3 实践

2.2.3.1 存储对象

使用散列类型非常适合存储对象,将结构体对象的字段和字段值与redis散列类型的字段和字段值一一对应,不仅结构灵活,而且修改的成本低,方便存取。当然,对于键的命名最好有一定的规范,如果命名的时候使用k1 k2 k3这种方式,那之后要查找数据的时候就很头疼了,还是建议使用 对象类型:对象ID:对象属性 这种命名方式比较好。

2.2.3.2 存储映射关系

散列类型还有一个常用用途就是存储映射关系,比如在一个博客系统中,可能需要存储文章缩略名和文章id的映射,这里不把缩略名放入文章信息结构体里面是因为文章缩略名要求唯一,所以以缩略名为field,文章id为value进行映射,当用户使用缩略名的时候,就能拿到文章id,根据文章id,就能很方便的找到真实的文章数据。

2.2.4 命令拾遗

2.2.4.1 hkeys || hvals

有时候我们只想获得key对应的所有field或者所有values
hkeys k1 获取所有field
hvals k1获取所有value

2.2.4.2 hlen 获取字段数量

hlen k1 返回k1中有多少个字段

2.3 列表类型

2.3.1 基本介绍

列表类型是一个有序的字符串列表,常用的操作就是向列表的左右两端进行插入和删除。
列表内部使用的是双向链表,所以向列表首尾两端添加和删除元素的时间复杂度为O(1),获取越靠近两端的元素就越快。这就意味着,就算有成千上万个元素,获取列表前十个元素的速度也很快。不过使用列表的代价是,当进行索引查找的时候会比较慢,因为需要从头开始遍历。所以列表类型适合用于存储那些只关注最新的元素的场景,比如日志,我们大多时候只关心最近的日志,或者最新的十篇文章等等这些场景。
列表可以实现两端的插入和删除,可以很方便的实现队列和栈的功能。
与散列类型相同,列表最多可以存储 2 32 2^32 232-1个元素。

2.3.1 命令

2.3.1.1 lpush | rpush 向列表两端添加数据

lpush key value… 从左边向列表中添加元素
rpush key value… 从右边向列表中添加元素

2.3.1.2 lpop | rpop 从列表两端删除元素

lpop key 从列表左端弹出元素
rpop key 从列表右端弹出元素

2.3.1.3 llen获取列表元素个数

lrem key count value 删除列表中前count个值为value的元素,返回值为实际删除的元素的个数,其中,count > 0的时候表示从左向右删除count个值为value的元素,count < 0的时候表示从右向左删除|count|个值为value的元素,count=0的时候,表示删除所有值为value的元素。

2.3.2 实践

2.3.2.1 存储对象ID

很多时候我们需要涉及到分页的功能,此时,我们可以把数据ID按照一定顺序存储到列表中,比如按照时间,那这样,我们就能很轻松的获得前10或者前20对应的数据了,当然也有一些小问题,比如按照时间存放,那么如果用户希望改动一个数据的时间怎么办呢?那就需要把对应的数据先删除,然后根据新的时间进行插入,这个过程的效率就比较低了。还有一点,之前提到,列表对于在海量数据中访问前10,前20这样的数据,效率较高,但是如果需要把所有的数据都拿出来分页,效率就一般了,因为双向链表肯定是需要从头到尾遍历的。
注意 这里需要提醒一点,当我们拿到对象ID之后,应该做什么呢?大多数的操作都是去散列表中根据ID去查找数据,但是,散列表中并不支持一次查找多个key对应的key值,所以需要循环,一次一次查询,redis是cs模式,每一次查询都是一次网络通信的过程,这样网络时延就比较高了,效率不高,后面会介绍新的方法。

2.3.2.1 存储评论或日志

根据之前的介绍,列表适合存储哪些只关心前10,或者前20的数据,并且数据不应该经常更改,所以在存储日志或者逻辑为不可更改的评论的时候,比较好用,因为大多数人都只会关心最近几条日志和最近发表的评论。

2.3.3 命令拾遗

2.3.3.1 lindex获取指定索引的值

lindex key index 获取key列表在下标为index处的值
index支持负数,-1表示最右端第一个元素的下标

2.3.3.2 ltrim 只保留列表的片段

ltrim key start end 只保留key列表从start到end范围内的值

2.3.3.3 linsert向指定值的前或者后 插入元素

linsert key before | after pivot value 向key列表中值为pivot的元素的前面或者后面插入value
该命令会返回插入后,列表中元素的个数

rpoplpush 将一个列表转移至另一个列表

rpoplpush source destination
根据该命令的名字可以知道,该命令是将source列表中的元素从右往左弹出,然后从左往右插入到destination中。

2.4 集合

2.4.1 基本介绍

和大多数的编程语言一样,redis也提供了集合的概念,在redis底层,集合其实就是值为空的散列表,所以查找速度为O(1),集合最常用的有插入,删除,判断是否存在这些指令之外,还有非常方便的求交集,并集,差集的操作。

2.4.2 命令

2.4.2.1 sadd | srem 增加或删除元素

sadd key member… 向key中添加一个或多个元素,命令会返回成功加入的数量(可能存在已经在集合中的元素,那么此次就不会重复添加)
srem key member… 从key中删除一个或多个元素,命令会返回成功删除的个数

2.4.2.2 smembers 获取集合中的所有元素

smembers key 获取key中的全部元素

2.4.2.3 sismember 判断元素是否在集合中

sismember key member判断member是否在key中

2.4.2.4 交集 | 并集 | 差集

sdiff k1 k2 k3 … 求所有集合的差集
k1 与 k2 的差集就是在k1中但是不在k2中的元素,当sdiff后面有多个集合的时候,运算顺序是先求k1 和 k2 的差集,得到新的结果,在求其与k3 的差集,直到最后。
sinter k1 k2 k3… 求所有集合的交集
k1 与 k2 的交集就是在k1中,并且也在k2中的元素,当sinter后面有多个集合的时候,运算顺序和求差集一样。
sunion k1 k2 k3… 求所有集合的并集
k1 与 k2 的并集为 k1和k2中的所有元素。同样支持传入多个键。

2.4.3 实践

2.4.3.1 存储文章标签

在博客系统中,每篇文章都会附带标签来标记这篇文章是关于什么的,比如C++的,还是java的,还是关于操作系统的,为了实现这一功能,我们可以为每一篇文章都创建一个集合键,其中存放该文章对应的标签。

2.4.3.2 通过标签搜索文章

还是上面的例子,有时候我们需要通过标签去搜索带有该标签的所有文章,当然遍历所有文章是可行的,但是效率很低。我们可以在创建文章的时候,同时为每一类标签创建集合键,其中存放带有该标签的文章id,这样做主要是方便进行求交集的操作,因为用户很可能会需要同时带有两类标签的文章数据。

2.4.4 命令拾遗

2.4.4.1 scard 获取集合中元素的个数

scard key 返回集合中元素的个数

2.4.4.2 进行集合运算,并存储结果

在集合运算的后面加上store,可以把结果存入到另一个集合中。
sdiffstore des k1 k2 k3
sinterstore des k1 k2 k3
sunionstore des k1 k2 k3

2.4.4.3 srandmember 随机获得集合中的元素

srandmember key随机获得key中的元素
srandmember 后面可以添加 count用来表示 随机获得元素的个数
如果count为正,表示获得的元素不会重复
如果count为负,表示可能会获得重复元素

2.4.4.4 spop key 从集合中随机弹出一个元素

spop key从key中随机弹出一个元素

2.5 有序集合

2.5.1 基本介绍

之前介绍过,可以使用列表来存储文章id,按照文章的发表时间来存储到列表中,这样可以很容易得到最近10篇文章,但是,如果文章的发布日期修改了,效率会很低,而且不支持按照阅读量排序。这里介绍有序集合,可以解决上述问题。
有序集合仍然是集合,不过,它与集合最大的不同就是,有序集合内部每一个元素都会关联一个分数score,这个分数可以表示任何含义,它就是有序集合排序的依据。在有序集合中,不仅可以进行插入,删除,判断是否存在等操作,还可以很容易的获得分数最高或者最低的N个元素,或者是在一定分数范围内的所有元素,在有序集合中,元素的值不可以相同,但是分数可以相同。
下面就有序集合列表的相同和不同之处进行比较。
相同之处:
(1)首先有序集合和列表都是有序的(列表的有序可以理解为列表会保持插入的顺序,而有序集合的有序则需要理解为有序集合会按照分数进行排序)
(2)有序集合和列表都可以获得某一个范围内的数据
不同之处:
(1)列表是通过双向链表来实现的,并且记录了首尾指针,所以获得列表前N个元素或者后N个元素的效率比较高,但是获得中间元素的效率不高,需要遍历。比较适合只关心新鲜事物的场景。
(2)有序集合是通过散列表加跳表实现的,所以就算访问中间元素,它的效率也很快,是O(nlogn)
(3)列表中调整某个元素的位置比较困难,而有序集合比较轻松
(4)有序集合比列表要更耗费内存

2.5.2 命令

2.5.2.1 zadd 增加元素

zadd key score member score2 member … zadd向key中添加member并附带其分数,如果集合中有member,则会更新其分数,zadd的返回值是新加入到集合中的元素个数。
其中分数不仅支持整数,还支持浮点数。

2.5.2.2 zscore 获得member的分数

zscore key member获得key中member元素的分数

2.5.2.3 zrange 获得排名在某个范围内的元素列表

zrange key start end 获得排名在start到end之间的元素列表,包含两端元素。索引从0开始,并且也支持负数索引,-1代表排序后的最后一个元素。
如果需要同时获得该元素的分数,可以在后面加上withscores
zrange key start end withsocers获得start到end序号范围内的所有元素及其分数。
如果两个元素分数相同,则redis会按照字典序顺序进行返回。

2.5.2.4 zrangebyscore 获得指定分数范围内的元素

zrangebyscore key start end 获得分数范围在start到end之间的元素列表,默认是包含两端的,如果不希望包含端点,也可以在分数前加上(,表示不包括该端点。
有时候,我们可能并不知道最大值已经到多少了,此时可以使用-inf表示负无穷,+inf表示正无穷。
同样,可以添加withscores来显示元素的score

2.5.2.5 zincrby 增加元素的分数

zincrby key incrment member让member元素的分数增加incrment。返回更改之后的分数。如果指定元素不存在,那么会将该元素初始值赋0之后,继续操作。

2.5.3 实践

2.5.3.1 按照点击量进行排序

有了有序列表之后,任何关于排序的工作都十分轻松,比如,如果想要实现按照文章点击量进行排序,则可以建立一个有序集合键,其中每一个文章ID都是一个元素,score就是它的点击量,当文章被访问的时候,可以对该文章id的score进行incrby加1,然后使用zrange进行排序,返回点击量最高的前N篇文章。

2.5.3.2 改进根据文章时间排序

之前介绍使用列表,可以实现返回最新发布的前N篇文章的功能,但是,使用列表查询所有文章的效率不高,而且更改文章的发布时间效率较低,现在有了有序集合,就可以很轻松的完成这份工作。同样建立一个有序集合键,其中元素仍然是文章id,但是score换成了文章的发布时间,有序列表会自动根据时间进行排序,更改文章发布时间的效率也很高。使用zrangebyscore还可以很轻松的获得指定范围时间内的文章。

2.5.4 命令拾遗

2.5.4.1 zcard 获取集合内的所有元素

zcard key 获取有序集合key内的所有元素

2.5.4.2 zcount 获取指定分数范围内的元素个数

zcount key start end 获取有序集合key中分数在start到end之间的元素个数
支持-inf 和 +inf

2.5.4.3 zrem 删除一个或多个元素

zrem key member… 删除有序集合key中的一个或多个元素

2.5.4.4 zremrangebyrank 删除排名范围内的元素

zremrangebyrank key start end删除排名从start到end范围内的所有元素,并返回删除元素的数量

2.5.4.5 zremrangebyscore 删除分数范围内的元素

zremrangebyscore key start end 删除元素分数在start到end之间的元素,并返回删除元素的个数。

2.5.4.6 zrank 获得元素的排名

zrank key member 获得有序集合中member的排名,从小到大,序号从0开始
zrevrank key member 获得有序集合中member的排名,从大到小

2.5.4.7 集合操作

zinterstore des memberkeys key1 key2… weights aggregate
其中 des 是最后结果会保存在有序集合res中,memberkeys表示参加运算的有序集合的数目,后面是对应数目的有序集合key,weights表示权重,因为res中元素的分数是由组成他的有序集合中的元素来的,可以设定不同集合的权重,在计算的时候,会拿每一个集合中的元素乘上权重,然后进行相加。aggregage表示计算res中分数的方法,可以选择默认,就是那所有集合中的元素进行相加,也可以选择min或者max,表示选择所有集合中该元素的最小或者最大分数作为res中该元素的分数。
zdiffstore和zunionstore用法相同。

以上是关于Redis入门详解之五种基本数据结构的主要内容,如果未能解决你的问题,请参考以下文章

Redis入门详解之五种基本数据结构

Redis五种数据结构详解

redis入门到精通系列:redis高级数据类型详解(BitMaps,HyperLogLog,GEO)

redis入门到精通系列:redis高级数据类型详解(BitMaps,HyperLogLog,GEO)

redis的入门篇---五种数据类型及基本操作

轻松搞定高并发:详解Redis的五种数据类型及应用场景分析!