Redis的五种数据类型、方法、底层数据结构

1、字符串string

2、列表list

3、散列hash

4、集合set

5、有序集合sorted set

 

字符串string

字符串类型是Redis中最为基础的数据存储类型,是一个由字节组成的序列,他在Redis中是二进制安全的,这便意味着该类型可以接受任何格式的数据,如JPEG图像数据货Json对象描述信息等,是标准的key-value,一般来存字符串,整数和浮点数。Value最多可以容纳的数据长度为512MB
应用场景:很常见的场景用于统计网站访问数量,当前在线人数等。incr命令(++操作)

Redis的五种数据类型、方法、底层数据结构

 

列表list

Redis的列表允许用户从序列的两端推入或者弹出元素,列表由多个字符串值组成的有序可重复的序列,是链表结构,所以向列表两端添加元素的时间复杂度为0(1),获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。List中可以包含的最大元素数量是4294967295。
应用场景:1.最新消息排行榜。2.消息队列,以完成多程序之间的消息交换。可以用push操作将任务存在list中(生产者),然后线程在用pop操作将任务取出进行执行。(消费者)

Redis的五种数据类型、方法、底层数据结构

 

散列hash

Redis中的散列可以看成具有String key和String value的map容器,可以将多个key-value存储到一个key中。每一个Hash可以存储4294967295个键值对。
应用场景:例如存储、读取、修改用户属性(name,age,pwd等)

Redis的五种数据类型、方法、底层数据结构

 

集合set

Redis的集合是无序不可重复的,和列表一样,在执行插入和删除和判断是否存在某元素时,效率是很高的。集合最大的优势在于可以进行交集并集差集操作。Set可包含的最大元素数量是4294967295。
应用场景:1.利用交集求共同好友。2.利用唯一性,可以统计访问网站的所有独立IP。3.好友推荐的时候根据tag求交集,大于某个threshold(临界值的)就可以推荐。

Redis的五种数据类型、方法、底层数据结构

 

有序集合sorted set

和set很像,都是字符串的集合,都不允许重复的成员出现在一个set中。他们之间差别在于有序集合中每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。尽管有序集合中的成员必须是唯一的,但是分数(score)却可以重复。
应用场景:可以用于一个大型在线游戏的积分排行榜,每当玩家的分数发生变化时,可以执行zadd更新玩家分数(score),此后在通过zrange获取几分top ten的用户信息。

Redis的五种数据类型、方法、底层数据结构

 

最后,还有个对key的通用操作,所有的数据类型都可以使用的

Redis的五种数据类型、方法、底层数据结构

 

=========

zset

zset底层存储结构

 zset底层的存储结构包括ziplist或skiplist,在同时满足以下两个条件的时候使用ziplist,其他时候使用skiplist,两个条件如下:

  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素的长度小于64字节

redis配置文件中用来控制zset到底是使用ziplist(压缩双向链表)还是skiplist(跳表)的参数:

zset-max-ziplist-entries 128

zset-max-ziplist-value 64

zset-max-ziplist-entries zset使用ziplist存储的时候,最大限制存储entries的个数
zset-max-ziplist-value zset使用ziplist存储的时候,每个节点最大存储字节数

违反上述两个限制条件,均会导致zset将ziplist的数据结构切换为skiplist数据结构

而zset使用ziplist的原因,主要是出于在零散数据量少的时候,节省内容的占用

当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

 当skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系


 

操作单个zset的redis命令

1、添加,如果值存在添加,将会重新排序。zadd

127.0.0.1:6379>zadd myZSet 1 zlh   ---添加分数为1,值为zlh的zset集合

127.0.0.1:6379>zadd mySet 2 Tom 3 Jim   ---添加2条zset集合,分别为分数为2、3,值为tom、jim的集合。

2、查看zset集合的成员个数。zcard

127.0.0.1:6379>zcard myZSet   ---输出zset的成员个数为3

3、查看Zset指定范围的成员,withscores为输出结果带分数。zrange

127.0.0.1:6379>zrange mZySet 0 -1   ----0为开始,-1为结束,输出顺序结果为: zlh tom jim

127.0.0.1:6379>zrange mZySet 0 -1 withscores   ---输出带分数的结果为:zlh 1 tom 2 jim 3

4、获取zset成员的下标位置,如果值不存在返回null。zrank

127.0.0.1:6379>zrank mZySet Jim    ---Jim的在zset集合中的下标为2

5、获取zset集合指定分数之间存在的成员个数。zcount

127.0.0.1:6379>zcount mySet 1 3   ---输出分数>=1 and 分数 <=3的成员个数为3,因为分数是可以重复的,所以这个命令是有道理的。

6、删除指定的一个成员或多个成员。zrem

127.0.0.1:6379>zrem myZSet zlh   --删除值为zlh的zset成员

127.0.0.1:6379>zrem myZSet Tom Jim    ---删除值为Tom和Jim的两个zset成员

7、获取指定值的分数。zscore

127.0.0.1:6379>zadd myZset 1 zlh 1 tom 2 jim 3 xdd 4 pmm   ---由于上面的数据被删除完了,这里添加5条示范数据再。

127.0.0.1:6379>zscore myZset zlh    ---输出值为zlh的分数为1

8、给指定元素的分数进行增减操作,负值为减,正值为加。zincrby

127.0.0.1:6379>zscore myZset tom    ----输出tom的分数为1

127.0.0.1:6379>zincrby myZset 4 tom   ---tom的分数值加4,输入分数值为5

127.0.0.1:6379>zscore myZset tom    ---输出tom的分数值为5

9、根据指定分数的范围获取值。zrangebysocre

127.0.0.1:6379>zrangebyscore myZset  1 5   ---输出分数>=1 and <=5的成员值为:zlh jim xdd pmm  tom

127.0.0.1:6379>zrangebyscore myZset  (1 5    ----输出分数>1 and <=5的成员值为:jim xdd pmm tom

127.0.0.1:6379>zrangebyscore myZset 2 5 limit 1 2    ---检索分数为2到5之间的数据,然后从下标为1的数据开始往后输出2个数据,包含下标为1的数据。结果为:xdd pmm

127.0.0.1:6379>zrangebyscore myZset -inf +inf limit 2 3   ----+inf表示最后一个成员,-inf表示第一个成员,意思是:检索所有数据,然后从下标为2的数据开始再往后输出2个数据。结果为:xdd pmm tom

10、倒序,从高到底排序输出指定范围的数据。zrevrange,zrevrangebyscore

127.0.0.1:6379>zrevrange myZset 2 3   ---先倒序排列数据,输出下标为>=2 and <=3的数据为xdd jim,这里注意的是倒序之后下标也反过来了。

127.0.0.1:6379>zrevrange myZset 2 4 withscores    ---输出结果为:xdd 3 jim 2 zlh 1

127.0.0.1:6379>zrevrangebyscore myZset 5 1 limit  3 2  ----输出结果为:jim zlh 。获取score <=5 and >=1,从下标为为3开始获取2条数据。

127.0.0.1:6379>zrevrangebyscore myZset 4 2   ----分数>=2 and <=4 的数据倒序输出:pmm xdd jim

11、根据坐标,分数范围删除数据。zremrangebyscore,zremrangebyrank

127.0.0.1:6379>zremrangebyscore myZset 1 2   ---删除分数>=1 and <=2的数据

127.0.0.1:6379>zrange myZset 0 -1    ----输出结果为 xdd pmm tom

127.0.0.1:6379>zremrangebyrank myZset 0 2    ---删除下标>=0 and <=2的zset元素

127.0.0.1:6379>zrange myZset 0 -1    --输出结果为:empty list or set 。没数据啦。

操作多个zset的redis命令

1、求多个zset的并集

127.0.0.1:6379>zadd myZset 1 zlh 2 jim 3 tom   ---添加3个数据

127.0.0.1:6379>zadd youZset 1 zlh 2 xdd 3 pmm    ---添加3个数据

127.0.0.1:6379>zunionzstore heZset 2 myZset youZset  ----将myzset和youzset的并集添加到hezset中。

2、求多个zset的交集

127.0.0.1:6379>zinterstore sheZset 2 myZset youZset  ----将myzset和youZset 的交集添加到sheZset中。

 

 

===图解redis五种数据结构底层实现===========

redis有五种基本数据结构:字符串、hash、set、zset、list。但是你知道构成这五种结构的底层数据结构是怎样的吗? 今天我们来花费五分钟的时间了解一下。 (目前redis版本为3.0.6)

动态字符串SDS

SDS是"simple dynamic string"的缩写。 redis中所有场景中出现的字符串,基本都是由SDS来实现的

所有非数字的key。例如 set msg"hello world" 中的key msg.字符串数据类型的值。例如`` set msg "hello world"中的msg的值"hello wolrd"非字符串数据类型中的“字符串值”。例如 RPUSH fruits"apple""banana""cherry"中的"apple" "banana" "cherry"SDS长这样:

Redis的五种数据类型、方法、底层数据结构

 

free:还剩多少空间 len:字符串长度 buf:存放的字符数组

空间预分配

为减少修改字符串带来的内存重分配次数,sds采用了“一次管够”的策略:

若修改之后sds长度小于1MB,则多分配现有len长度的空间若修改之后sds长度大于等于1MB,则扩充除了满足修改之后的长度外,额外多1MB空间

Redis的五种数据类型、方法、底层数据结构

惰性空间释放

为避免缩短字符串时候的内存重分配操作,sds在数据减少时,并不立刻释放空间。

Redis的五种数据类型、方法、底层数据结构

int

就是redis中存放的各种数字 包括一下这种,故意加引号“”的

Redis的五种数据类型、方法、底层数据结构

 

双向链表

长这样:

Redis的五种数据类型、方法、底层数据结构

 

分两部分,一部分是“统筹部分”:橘黄色,一部分是“具体实施方“:蓝色。

主体”统筹部分“:

head指向具体双向链表的头tail指向具体双向链表的尾len双向链表的长度具体"实施方":一目了然的双向链表结构,有前驱 pre有后继 next

由 list和 listNode两个数据结构构成。

ziplist

压缩列表。 redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。

然后文中的 entry的结构是这样的:

Redis的五种数据类型、方法、底层数据结构

 

元素的遍历

先找到列表尾部元素:

Redis的五种数据类型、方法、底层数据结构

然后再根据ziplist节点元素中的 previous_entry_length属性,来逐个遍历:

Redis的五种数据类型、方法、底层数据结构

连锁更新

再次看看 entry元素的结构,有一个 previous_entry_length字段,他的长度要么都是1个字节,要么都是5个字节:

前一节点的长度小于254字节,则 previous_entry_length长度为1字节前一节点的长度小于254字节,则 previous_entry_length长度为5字节假设现在存在一组压缩列表,长度都在250字节至253字节之间,突然新增一新节点 new, 长度大于等于254字节,会出现:

Redis的五种数据类型、方法、底层数据结构

程序需要不断的对压缩列表进行空间重分配工作,直到结束。

除了增加操作,删除操作也有可能带来“连锁更新”。 请看下图,ziplist中所有entry节点的长度都在250字节至253字节之间,big节点长度大于254字节,small节点小于254字节。

Redis的五种数据类型、方法、底层数据结构

哈希表

哈希表略微有点复杂。哈希表的制作方法一般有两种,一种是: 开放寻址法,一种是 拉链法。redis的哈希表的制作使用的是 拉链法。

整体结构如下图:

Redis的五种数据类型、方法、底层数据结构

 

也是分为两部分:左边橘黄色部分和右边蓝色部分,同样,也是”统筹“和”实施“的关系。 具体哈希表的实现,都是在蓝色部分实现的。 先来看看蓝色部分:

Redis的五种数据类型、方法、底层数据结构

 

这也分为左右两边“统筹”和“实施”的两部分。

右边部分很容易理解:就是通常拉链表实现的哈希表的样式;数组就是bucket,一般不同的key首先会定位到不同的bucket,若key重复,就用链表把冲突的key串起来。

新建key的过程:

Redis的五种数据类型、方法、底层数据结构

假如重复了:

Redis的五种数据类型、方法、底层数据结构

rehash

再来看看哈希表总体图中左边橘黄色的“统筹”部分,其中有两个关键的属性: ht和 rehashidx。 ht是一个数组,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的 rehash有关。

rehash指的是重新计算键的哈希值和索引值,然后将键值对重排的过程。

加载因子(load factor)=ht[0].used/ht[0].size。

扩容和收缩标准

扩容:

没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的 加载因子大于等于1。正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的 加载因子大于等于5。收缩:

加载因子小于0.1时,程序自动开始对哈希表进行收缩操作。扩容和收缩的数量

扩容:

第一个大于等于 ht[0].used*2的 2^n(2的n次方幂)。收缩:

第一个大于等于 ht[0].used的 2^n(2的n次方幂)。(以下部分属于细节分析,可以跳过直接看扩容步骤)

对于收缩,我当时陷入了疑虑:收缩标准是 加载因子小于0.1的时候,也就是说假如哈希表中有4个元素的话,哈希表的长度只要大于40,就会进行收缩,假如有一个长度大于40,但是存在的元素为4即( ht[0].used为4)的哈希表,进行收缩,那收缩后的值为多少?

我想了一下:按照前文所讲的内容,应该是4。 但是,假如是4,存在和收缩后的长度相等,是不是又该扩容? 翻开源码看看:

收缩具体函数:

int dictResize(dict *d) { int minimal; //如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; minimal = d->ht[0].used; //获得已经有的节点数量作为最小限度minimal if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4) minimal = DICT_HT_INITIAL_SIZE; return dictExpand(d, minimal); //用minimal调整字典d的大小} int dictExpand(dict *d, unsigned long size) { dictht n; unsigned long realsize = _dictNextPower(size); //获得一个最接近2^n的realsize if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志 return DICT_ERR; if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一样则返回出错标志 /* Allocate the new hash table and initialize all pointers to NULL */ //初始化新的哈希表的成员 n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { //如果ht[0]哈希表为空,则将新的哈希表n设置为ht[0] d->ht[0] = n; return DICT_OK; } d->ht[1] = n; //如果ht[0]非空,则需要rehash d->rehashidx = 0; //设置rehash标志位为0,开始渐进式rehash(incremental rehashing) return DICT_OK;} static unsigned long _dictNextPower(unsigned long size){ unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE 为 4 if (size >= LONG_MAX) return LONG_MAX + 1LU; while(1) { if (i >= size) return i; i *= 2; }}由代码我们可以看到,假如收缩后长度为4,不仅不会收缩,甚至还会报错。()

我们回过头来再看看设定:题目可能成立吗? 哈希表的扩容都是2倍增长的,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128

也就是说:不存在长度为 40多的情况,只能是64。但是如果是64的话,64 X 0.1(收缩界限)= 6.4 ,也就是说在减少到6的时候,哈希表就会收缩,会缩小到多少呢?是8。此时,再继续减少到4,也不会再收缩了。所以,根本不存在一个长度大于40,但是存在的元素为4的哈希表的。

扩容步骤

Redis的五种数据类型、方法、底层数据结构

收缩步骤

Redis的五种数据类型、方法、底层数据结构

渐进式refresh

在"扩容步骤"和"收缩步骤" 两幅动图中每幅图的第四步骤“将ht[0]中的数据利用哈希函数重新计算,rehash到ht[1]”,并不是一步完成的,而是分成N多步,循序渐进的完成的。 因为hash中有可能存放几千万甚至上亿个key,毕竟Redis中每个hash中可以存 2^32-1 键值对(40多亿),假如一次性将这些键值rehash的话,可能会导致服务器在一段时间内停止服务,毕竟哈希函数就得计算一阵子呢((#^.^#))。

哈希表的refresh是分多次、渐进式进行的。

渐进式refresh和下图中左边橘黄色的“统筹”部分中的 rehashidx密切相关:

rehashidx 的数值就是现在rehash的元素位置rehashidx 等于 -1 的时候说明没有在进行refresh

 

 

甚至在进行期间,每次对哈希表的增删改查操作,除了正常执行之外,还会顺带将ht[0]哈希表相关键值对rehash到ht[1]。

以扩容步骤为例:

Redis的五种数据类型、方法、底层数据结构

intset

整数集合是集合键的底层实现方式之一。

Redis的五种数据类型、方法、底层数据结构

 

跳表

跳表这种数据结构长这样:

Redis的五种数据类型、方法、底层数据结构

 

redis中把跳表抽象成如下所示:

Redis的五种数据类型、方法、底层数据结构

 

看这个图,左边“统筹”,右边实现。 统筹部分有以下几点说明:

header: 跳表表头tail:跳表表尾level:层数最大的那个节点的层数length:跳表的长度实现部分有以下几点说明:

表头:是链表的哨兵节点,不记录主体数据。是个双向链表分值是有顺序的o1、o2、o3是节点所保存的成员,是一个指针,可以指向一个SDS值。层级高度最高是32。没每次创建一个新的节点的时候,程序都会随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是“高度”redis五种数据结构的实现

redis对象

redis中并没有直接使用以上所说的各种数据结构来实现键值数据库,而是基于一种对象,对象底层再间接的引用上文所说的具体的数据结构。

结构如下图:

Redis的五种数据类型、方法、底层数据结构

 

字符串

Redis的五种数据类型、方法、底层数据结构

 

其中:embstr和raw都是由SDS动态字符串构成的。唯一区别是:raw是分配内存的时候,redisobject和 sds 各分配一块内存,而embstr是redisobject和raw在一块儿内存中。

列表

Redis的五种数据类型、方法、底层数据结构

 

hash

Redis的五种数据类型、方法、底层数据结构

 

set

Redis的五种数据类型、方法、底层数据结构

 

zset

 

Redis的五种数据类型、方法、底层数据结构