Redis 的五种数据类型及其底层原理
Redis是 key-value结构的数据库。Redis常用的五种数据类型分别是:String、List、Set、Hash、Zset。一、Redis五种类型的常用命令1.1 StringString是 redis 最基本的数据类型。一个 key 对应一个value。redis的String可以表示任何数据,比如 jpg图像或者序列化的对象,String的最大值能存储512MB。常用命令:get、s
目录
2.1 redisObject如何表示string (各个encoding方式解释)
Redis是 key-value结构的数据库。
Redis常用的五种数据类型分别是:String、List、Set、Hash、Zset。
一、Redis五种类型的常用命令
1.1 String
String是 redis 最基本的数据类型。一个 key 对应一个value。redis的String可以表示任何数据,比如 jpg图像或者序列化的对象,String的最大值能存储512MB。
常用命令:
get、set、incr、decr、mget
set:往redis里输入key-value,如:输入 key 为name,value为zhujunwen
get:输入 key 值,返回对应的 value 值。如:
incr:自增1,如:
decr:自减。
mget:一次性获取多个key的value。如:
1.2 List
常用命令:
lpush、lpop、rpush、rpop、llen
lpush:从列表List的左边插入一个元素。
lpop:从列表List的左边移出一个元素。
rpush:从列表List的右边插入一个元素。
rpop:从列表List的右边移出一个元素。
llen:打印当前列表List中的元素个数。
1.3 Set
常用命令:
sadd、srem、scard、sismember
sadd:往set中添加数据。
srem:从set中删除数据。
scard:查看set中存在的元素个数。
sismember:查看set中是否存在某个数据。
1.4 hash
常用命令:
hget、hset、hmget
hget:通过key值,从hash里取对应的value
hset:往hash里,添加key-value
hmget:一次性获取多个key的value
1.5 zset (有序集合)
在redis中,set和zset都是元素的集合,都不允许有重复的元素。不同的是,zset的每个元素都会关联一个分数(分数可以重复),redis通过这个分数为集合中的成员进行排序。
常用命令:
zadd、zcard、zrange、zrem、zrevrange
zadd:添加数据
zrem:删除元素
zrem 还可以一次性删除多个元素:
zcard:查询数据
zrange:数据排序,根据分数从小到大
withscores表示用分数进行排序。下面命令的 0、2 表示排序的区间是第0个到第二个元素。
zrevrange:数据排序, 根据分数从大到小
例如返回分值最高的前3个元素:
二、五种数据类型的原理(包括其编码方式)
Redis内部使用一个 redisObject对象来表示所有的key和value,redisObject的信息如下图所示:
可以看到redisObject的信息有 type、encoding、ptr、vm。
type:用来表示 这个redisObject是属于 五种类型(string、hash、list、set、zset)的哪一种,比如 type=string代表value 存储的是一个普通字符串。
encoding:用来表示 type 的底层数据结构是用什么实现的。打个比方,就如Java中的 list ,可以由ArrayList 来实现,也可以由LinkedList 来实现。
ptr:指向底层数据结构的指针。
vm:只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。
2.1 redisObject如何表示string (各个encoding方式解释)
字符串的encoding有三种方式:
- int
- raw
- embstr
2.1.1 int
如果一个字符串string保存的是整数值,如 set number 10086 ,那么这个整数值可以用 long 类型标识。那么该字符串的 redisObject会把10086这个数值保存在ptr属性中,并将encoding设置成int。假设有如下命令:set number 10086。那么 number 键对象的示意图如下:
(即当string对象的值全部是数字,就会使用int编码。)
2.1.2 raw
如果字符串string 保存的是一个字符串值。并且这个字符串大于39个字节,那么字符串对象将使用一个 简单动态字符串(SDS) 来保存这个字符串值,并将 redisObject的encoding设置为raw。
使用 raw 存储字符串的示意图如下:
由于字符串大于39个字节的话,即不能确定值的字节大小,所以redisObject 和 字符串的内存是分开分配的。
注意:raw编码方式,是redisObject 的内存地址和 sds 的内存地址是不连续的。
2.1.3 embstr
如果字符串string 保存的是一个字符串值,并且这个字符串小于39个字节,那么字符串将使用 embstr 编码的方式来保存这个字符串。
用embstr存储的话,一般字符串的存储内存很小,因此redis一次性分配 redisObject和 sds 的内存,且内存连续。
embstr对比raw的优点:
- embstr创建 字符串对象(redisObject) 的次数只需1次,而raw 是两次(redisObject 和sds 分开分配)。
- 同理,embstr调用释放内存的函数也是1次,比raw编码的字符串对象少一次。
- 由于 embstr编码的是内存连续的,而raw是不连续的,因此存取速度embstr比较快。
注意:embstr 编码方式,是 redisObject 的内存地址和 sds 的内存地址是连续的。
2.2 redisObject如何表示 list
列表对象list 的编码方式encoding有两种,分别是:ziplist、linkedlist。
2.2.1 ziplist 压缩列表
压缩列表是 节省内存而设计的内存结构(是redis创造的)。优点是 节省内存,缺点是 比其他结构要消耗更多的时间,所以redis在数据量少的时候使用压缩列表存储。
即 列表长度少于 512,并且所有元素的长度都少于64个字节时,使用压缩列表存储,否则使用 linkedlist 存储。
用压缩列表ziplist 编码的redisObject如下:
由上图所示:在ziplist中,结点之前有三个变量,分别是: zlbytes、zltail、zllen,这三个的详细说明如下:
ziplist能够节省内存的原理
我们知道,普通数据能够支持随机访问的原因就是存储的内存是连续的。但是有一个问题,就是数组中每个元素的大小都必须是一样的,如果有大小不一样的话,那么该元素的内存就必须按照数组中最大的元素(假设是5个字节)的内存存放,那么存储少于5字节的元素就会存在内存浪费问题。如下图:
我们要想既保留数组的优势,又不浪费内存,就只能把数组设计成 每个元素的大小都不定的,如下:
那么ziplist是设计去如何解决这个问题呢?对于一个 每个元素不定长的数组 我们在遍历它的时候由于不知道每个元素的大小是多少,因此也就无法计算出下一个节点的具体位置。这个时候我们可以给每个节点增加一个lenght的属性
data-length 记录着 data-value的大小。因此我们在遍历节点的之后就知道每个节点的长度(占用内存的大小),就可以很容易计算出下一个节点再内存中的位置。这种结构就像一个简单的压缩列表了。
2.2.2 linkedlist
当列表长度 少于 512且 每个元素都少于64个字节,那么就用ziplist存储。否则就用linkedlist存储。
linkedlist的编码方式如下:
2.3 redisObject如何表示 hash
hash 的encoding 编码方式共有两种:ziplist、hashtable
2.3.1 ziplist
当哈希对象保存的键值对数量少于512,且所有键值对的长度都少于64字节时,使用压缩列表保存。
在压缩列表中,每当有新的键值对要加入到哈希对象时, 程序会先将保存了 键的压缩列表节点 推入到压缩列表表尾, 然后再将 保存了值的压缩列表节点 推入到压缩列表表尾, 因此:保存了同一键值对的两个节点总是紧挨在一起。如下图:
2.3.2 hashtable
若哈希对象保存的键值对个数大于512,并且其中有键值对大于64个字节,就使用hashtable 保存。如下:
如:
2.4 redisObject如何表示 set
set 的encoding编码方式有:intset、hashtable
2.4.1 intset
当 集合的长度少于 512 时,并且所有元素都是整数,使用 intset存储。否则使用 hashtable。
2.4.2 hashtable
hashtable编码的底层实现是字典,字典的每个键是字符串对象,只不过值都是空(NULL)。
2.5 zset
zset的encoding 编码有两种,分别是:ziplist、skiplist。
2.5.1 ziplist
当zset的长度少于128,并且所有元素的长度都少于64字节时,用ziplist存储。如下图:
我们可以看到,每个节点,前面是字符串,后面是分数值。分值小的靠近表头,大的靠近表尾。
2.5.2 skiplist
redis 的 skiplist 是由 字典dict 和 跳表构成的。
zset的结构体定义如下:
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
什么是跳表?
如下图:
原始链表中,如果要查询结点15,那么一共就需要从结点1开始,到结点15,一共查询15个结点才能找到结点15。(跳表必须是排好序的)
那么如果我们额外加一层索引层呢?如下图,我们从下图的 第一级索引层 开始查找,那么要找到结点15,只需要找 结点 1-4-7-10-14-14-15,只需要查询七个结点即可。这就是跳表,可以大大地方便查询。
当然,那还能不能加第二层、第三层索引层呢?当然可以。如下图,我们从第二级索引层开始,查询结点15。那么查询顺序为 1-7-14-14-14-15,一共需要查询6个结点,查询效率又提高了。
这种通过对链表加多级索引的机构,就是跳表了。跳表 每层索引层的结点数目都是其前一层的一半。 因为这样的话,查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。 但是这样的话,会存在一个问题,就是 当新插入一个结点的时候,会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。 如果硬要维持这种2:1的关系话,那么插入和删除结点后,都必须要对整个跳表重新调整,这会降低效率。
因此 skiplist 为了解决这个问题,并不要求 相邻上下链表结点个数必须按照2:1的关系。而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个skiplist的过程:
从上面skiplist的创建和插入过程可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。实际上,这是skiplist的一个很重要的特性,这让它在插入性能上明显优于平衡树的方案。
skiplist的字典dict 和 跳表。
刚刚说了 skiplist 是由 dict 和跳表组成的。
- dict 用于记录 字符串对象和分数,即查询 字符串对象对应 分数。
- 跳表则用来,根据 分数查询对应字符串。
为什么 skiplist 编码要同时用字典和跳表来实现?
- 字典查询分值的时间复杂度是O(1)。但是无序。
- 跳表的优点是有序,但是查询的时间复杂度为O(logn)。
- 虽然采用两个结构,但是集合的元素成员和分值是共享的,两种结构都通过指针指向同一地址,所以不会存在内存浪费。
redis里的一个跳表的结构如下:
- 包含一个 头节点header 和 尾结点 tail。
- length 表示节点数。
- level 表示表示skiplist的总层数,即所有节点层数的最大值。
skiplist 结构如下:
三、五种数据类型的应用场景
3.1 string的应用场景
普通的 key-value键值对都可以用 string来保存,例如:
- 访问量统计,每次访问博客和文章,都用 incr 命令加一。
- 做缓存。
3.2 list 的应用场景
作为队列,因为 list 的两端操作比较方便,所以可以用于一些需要获取最新数据的场景,如新闻类应用的最新新闻。
3.3 hash 的应用场景
用于存储、修改对象属性。比如:用户(姓名、性别、爱好),文章(标题、发布时间、作者、内容)。其中 用户相当于key,(姓名、性别、爱好)相当于存储的value。
3.4 set 的应用场景
- 好友推荐,根据set的内容求交集,大于某个阈值就可以推荐。
- 利用set的唯一性,统计网站内所有独立ip。
3.5 zset的应用场景
- 排行榜,因为zset 本来就是有序的,并且有排序功能。
更多推荐
所有评论(0)