Redis数据结构的对象
为什么使用Redis,而不用map做缓存
- Java实现的Map是本地缓存,如果有多台实例(机器)的话,每个实例都需要各自保存一份缓存,缓存不具有一致性
- Redis实现的是分布式缓存,如果有多台实例(机器)的话,每个实例都共享一份缓存,缓存具有一致性。
- Java实现的Map不是专业做缓存的,JVM内存太大容易挂掉的。一般用做于容器来存储临时数据,缓存的数据随着JVM销毁而结束。Map所存储的数据结构,缓存过期机制等等是需要程序员自己手写的。
- Redis是专业做缓存的,可以用几十个G内存来做缓存。Redis一般用作于缓存,可以将缓存数据保存在硬盘中,Redis重启了后可以将其恢复。原生提供丰富的数据结构、缓存过期机制等等简单好用的功能。
运行机制
Redis并没有直接使用这些数据结构来实现key-value
数据库,而是基于这些数据结构创建了一个对象系统。Redis使用对象来表示数据库中的键和值。每次我们在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。简单来说就是Redis对key-value
封装成对象,key是一个对象,value也是一个对象。每个对象都有type(类型)、encoding(编码)、ptr(指向底层数据结构的指针)来表示。
Redis数据结构
1、Redis字符串(string)
- sdshdr数据结构中用len属性记录了字符串的长度。那么获取字符串的长度时,时间复杂度只需要O(1)。
- SDS不会发生溢出的问题,如果修改SDS时,空间不足。先会扩展空间,再进行修改!(内部实现了动态扩展机制)。
- SDS可以减少内存分配的次数(空间预分配机制)。在扩展空间时,除了分配修改时所必要的空间,还会分配额外的空闲空间(free 属性)。
- SDS是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据。
2、Redis链表的特性(list)
- 无环双向链表
- 获取表头指针,表尾指针,链表节点长度的时间复杂度均为O(1)
- 链表使用
void *
指针来保存节点值,可以保存各种不同类型的值
3、Redis哈希表(hash)
Redis实现的哈希表和Java中实现的是类似的。只不过Redis多了几个属性来记录常用的值:sizemark(掩码)、used(已有的节点数量)、size(大小)。同样地,Redis为了更好的操作,对哈希表往上再封装了一层(参考上面的Redis实现链表),使用dict结构来表示。所以,最后我们可以发现,Redis所实现的哈希表最后的数据结构是这样子的:
从代码实现和示例图上我们可以发现,Redis中有两个哈希表:
- ht[0]:用于存放真实的
key-vlaue
数据 - ht[1]:用于扩容(rehash)
Redis中哈希算法和哈希冲突跟Java实现的差不多,它俩差异就是:
- Redis哈希冲突时:是将新节点添加在链表的表头。
- JDK1.8后,Java在哈希冲突时:是将新的节点添加到链表的表尾。
4、rehash的过程
Redis具体是rehash时这么干的:
- 在字典中维持一个索引计数器变量rehashidx,并将设置为0,表示rehash开始。
- 在rehash期间每次对字典进行增加、查询、删除和更新操作时,除了执行指定命令外;还会将ht[0]中rehashidx索引上的值rehash到ht[1],操作完成后rehashidx+1。
- 字典操作不断执行,最终在某个时间点,所有的键值对完成rehash,这时将rehashidx设置为-1,表示rehash完成
- 在渐进式rehash过程中,字典会同时使用两个哈希表ht[0]和ht[1],所有的更新、删除、查找操作也会在两个哈希表进行。例如要查找一个键的话,服务器会优先查找ht[0],如果不存在,再查找ht[1],诸如此类。此外当执行新增操作时,新的键值对一律保存到ht[1],不再对ht[0]进行任何操作,以保证ht[0]的键值对数量只减不增,直至变为空表。
5、跳跃表(sortset)
跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一!
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点。
zskiplistNode的对象示例图(带有不同层高的节点):
示例图如下:
zskiplist的结构如下:
6、整数集合(intset)
整数集合是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,并且元素的数量不多时,Redis就会采用整数集合(intset)作为set(集合)的底层实现。整数集合(intset)保证了元素是不会出现重复的,并且是有序的(从小到大排序)
intset示例图:
说明:虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值:
- INTSET_ENC_INT16
- INTSET_ENC_INT32
- INTSET_ENC_INT64
从编码格式的名字我们就可以知道,16,32,64编码对应能存放的数字范围是不一样的。16明显最少,64明显最大。
如果本来是INTSET_ENC_INT16的编码,想要存放大于INTSET_ENC_INT16编码能存放的整数值,此时就得编码升级(从16升级成32或者64)。步骤如下:
- 根据新元素类型拓展整数集合底层数组的空间并为新元素分配空间。
- 将底层数组现有的所以元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位上,需要维持底层数组的有序性质不变。
- 将新元素添加到底层数组。
另外一提:只支持升级操作,并不支持降级操作。
7、压缩列表(ziplist)
压缩列表(ziplist)是list和hash的底层实现之一。如果list的每个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)作为list的底层实现。压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构。
压缩列表结构图例如下:
下面我们看看节点的结构图:
压缩列表从表尾节点倒序遍历,首先指针通过zltail偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表。
Redis数据结构的对象
1、字符串(string)对象
在上面的图我们知道string类型有三种编码格式:
- int:整数值,这个整数值可以使用long类型来表示,如果是浮点数,那就用embstr或者raw编码。具体用哪个就看这个数的长度了
- embstr:字符串值,这个字符串值的长度小于32字节
- raw:字符串值,这个字符串值的长度大于32字节
embstr和raw的区别:
- raw分配内存和释放内存的次数是两次,embstr是一次
- embstr编码的数据保存在一块连续的内存里面
编码之间的转换:
- int类型如果存的不再是一个整数值,则会从int转成raw
- embstr是只读的,在修改的时候回从embstr转成raw
2、列表(list)对象
在上面的图我们知道list类型有两种编码格式:
- ziplist:字符串元素的长度都小于64个字节
&&
总数量少于512个 - linkedlist:字符串元素的长度大于64个字节
||
总数量大于512个
ziplist编码的列表结构:
linkedlist编码的列表结构:
编码之间的转换:原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成linkedlist编码的。
3、哈希(hash)对象
在上面的图我们知道hash类型有两种编码格式:
- ziplist:key和value的字符串长度都小于64字节
&&
键值对总数量小于512 - hashtable:key和value的字符串长度大于64字节
||
键值对总数量大于512
ziplist编码的哈希结构:
hashtable编码的哈希结构:
编码之间的转换:原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成hashtable编码的。
4、集合(set)对象
在上面的图我们知道set类型有两种编码格式:
- intset:保存的元素全都是整数
&&
总数量小于512 - hashtable:保存的元素不是整数
||
总数量大于512
intset编码的集合结构:
hashtable编码的集合结构:
编码之间的转换:原本是intset编码的,如果保存的数据不是整数值或者元素数量大于512,会转换成hashtable编码的。
5、有序集合(sortset)对象
在上面的图我们知道set类型有两种编码格式:
- ziplist:元素长度小于64
&&
总数量小于128 - skiplist:元素长度大于64
||
总数量大于128
ziplist编码的有序集合结构:
skiplist编码的有序集合结构:
有序集合(sortset)对象同时采用skiplist和哈希表来实现:skiplist能够达到插入的时间复杂度为O(logn),根据成员查分值的时间复杂度为O(1)
编码之间的转换:原本是ziplist编码的,如果保存的数据长度大于64或者元素数量大于128,会转换成skiplist编码的。
6、Redis对象一些细节
- 服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。比如我们的数据结构是sortset,但你使用了list的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令
- Redis的对象系统带有引用计数实现的内存回收机制。对象不再被使用的时候,对象所占用的内存会释放掉
- Redis会共享值为0到9999的字符串对象
- 对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间。
我们在使用的时候挑选哪些数据结构作为存储,可以简单看看:
- string-->简单的
key-value
- list-->有序列表(底层是双向链表)-->可做简单队列
- set-->无序列表(去重)-->提供一系列的交集、并集、差集的命令
- hash-->哈希表-->存储结构化数据
- sortset-->有序集合映射(member-score)-->排行榜