redisObject
在了解redis基本数据类型之前,需要先了解redisObject对象,redis中所有键值都是redisObject对象。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向数据的指针
void *ptr;
// 记录对象最后一次被程序访问时间,用于计算空转时长(当前时间-lru)
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用计数,用于内存回收
int refcount;
} robj;
type: 对象类型名称
如:string、hash、list、set、zset
encoding:对象编码类型
如:int、embstr、raw、hashtable、ziplist、intset、linkedlist、skiplist
*ptr:数据存放地址指针
lru:对象最后一次被访问的时间
refcount:引用计数
1、string
string类型在redis中是最基本的数据类型,其中redis的所有key都是string类型的。
string适用场景
计数器、粉丝数、图片、序列化后的对象
string底层使用三种结构存储:
①int:当存储的字符串全是数字时,使用int的方式进行存储
②embstr:当存储的字符串长度小于39个字节时,使用embstr方式存储(redis3.2版本后优化为44个字节)
③raw:当存储的字符串长度大于39个字节时,使用raw方式来存储(redis3.2版本后优化为44个字节)
int类型不用多说,embstr为什么是以39个字节为界?后续在哪优化为44个字节?embstr和raw有什么区别?话不多说,上图:
embstr与raw的区别
先说一下区别,embstr和raw都是用来存储string数据的,并且都是使用SDS来存储,区别在于embstr是一块连续的内存,而raw是两个不连续的内存,因为raw是用来存长字符串的,所以SDS相对来说占用内存较大,而申请一块连续较大的内存是得不偿失的,embstr存数据时是只读的,如果需要改变embstr存储的value时,redis底层会将string的存储方式由embstr转换为raw,然后再去修改其value。embstr相对raw的优势在于申请内存和释放内存时只需要一次,而raw申请/释放两块不连续的内存需要两次,在效率上,embstr肯定是高于raw的。
SDS数据结构
RedisObject的数据结构我们上面讲过了,其中指针*ptr指向数据存放地址,在embstr和raw这两个结构中string的value都是存储在SDS中的,SDS是什么呢?
SDS(simple dynamic string):
struct shsshr<T>{
T len; // 数组长度
T alloc; // 数组容量
unsigned flags; // sdshdr类型
char buf[]; // 数组内容
}
len:数组长度
alloc:数组容量
buf[]:存放value的字符数组
为什么3.2版本之前的embstr是以39个字节为界,因为一块内存大小为64字节的sds,减去len、alloc等所占用的内存后,剩下的buf[]空间为39字节,在3.2版本时,redis对对sds内存进行了优化,使用sdshdr8代替sdshdr,节省了5个字节。
SDS优点
①记录value len,获取字符串长度时间复杂度为O(1)
②记录剩余空间,杜绝缓冲区溢出
③通过采用空间预分配和惰性空间释放策略,减少修改字符串带来的内存重新分配次数
④二进制安全
2、hash
hash是一个string类型的field和value的映射表,是键值对的集合,类似于一个map(field为键,value为值)。
hash使用场景
对象、键值对数据
hash使用两种方式进行存储:ziplist、hashtable
ziplist:
- hash对象保存的所有键值对的键和值的字符串长度都小于64字节
- hash对象保存的键值对数量小于512个
hash每次写数据时会对数据进行上面两个判断,若不符合这两个条件,hash类型的数据会转为hashtable进行存储。
ziplist
- zlbytes:ziplist的字节长度,占32位,4个字节,所以ziplist最长为(2^32) - 1字节
- zltail:ziplist尾元素相对于ziplist其实地址的偏移量,占4个字节
- zllen:ziplist的元素数量,占两个字节,当元素数目超过(2^16) - 1时,无法通过zllen字段获取ziplist的元素数目,必须遍历整个ziplist才能获取元素数目
- entry:ziplist存储的元素,可以为byte数组或者整数
- zlend:ziplist的结尾,占一个字节
ziplist中最重要的就是entry,接下来看看entry的结构:
- prevlength:上一个节点的长度
- encoding:节点的编码类型和数据长度(通过一串字符来确定data的数据类型)
- data:节点的值(数字或字符串)
根据前一个节点的长度prevlength,可以将ziplist节点分为两类(节省空间):
- entry前8位小于254,则这8位就表示前一个节点的长度
- entry的前8位等于254,则意味着前一个节点的长度无法用8位来表示,后面的32位表示前一个节点的长度
ziplist是一个类似于数组和双向链表的结构,顾名思义,使用ziplist(压缩列表)是为了节省空间。
- ziplist与数组的区别:数组的数据类型固定,单个节点的长度固定,而ziplist可以存储不同类型的数据,并且单个节点的长度不固定
- ziplist与双向链表的区别:双向链表的每个节点会存放前后节点的指针,并且节点之间内存不连续,而ziplist是一块连续的内存,节点会通过prevlength和自身length来索引前后的节点,相比于双向链表,ziplist减少了内存碎片和指针内存的占用。虽然在遍历和读取效率上低于链表,但是在空间上有很大的优势。
通过以上的一些分析,我们可以知道ziplist是为了节省空间,牺牲了读取效率,并且不适合做修改操作,如修改单个value或者往中间插入元素,这种操作会使该节点后面的元素全部后移,效率很低。
hashtable
redis的使用的hashtable就是一个很典型的hashtable数据结构,使用数组+链表的方式存储数据,通过field的hash值来确定数组下标,数组的单个元素为链表结构。
hash数据类型使用hashtable是在单个元素过大或者元素数量过多时使用,原因就是hashtable虽然没有ziplist节省空间的优势,但是在遍历和读取上的效率是很高的,并且也能较好的支持修改操作,hashtable数据结构比较典型,就不过多赘述了。
3、list
list数据类型底层采用ziplist或linkedlist两种结构进行存储
list适用场景
队列、栈、时间线
ziplist:
- list保存的元素长度小于64字节
- list保存的元数量小于512个
满足这两个条件则使用list进行存储,否则使用linkedlist
ziplist上面已经讲过了,linkedlist数据结构也比较简单。当list采用linkedlist存储时,由于双向链表的特性,list的增删是特别快的,另外list可以从头/尾插入/删除,所以比较适合做栈、队列。
4、set
set是string类型的无序集合,底层使用intset和hashtable来存储,set就像数学中的集合一样,比较方便做并集、交集、差集等操作。
set适用场景
共同关注、共同好友
inset:内部是一个整型数组
- set对象所保存的元素全是整型
- set对象所保存的元素小于512个
当set使用hashtable进行存储时,hashtable的key为元素值,value为null,通过key的hash值进行去重。
5、zset(sorted set)
zset底层的存储结构为ziplist或skiplist,zset的特点是每个value有一个score(分值)属性,通过分值进行排序。
zset适用场景
排行榜
ziplist:
- zset保存的元素数量小于128个
- zset所有元素的长度小于64个字节
当使用ziplist来作为zset底层的数据结构时,每个集合元素使用两个entry来保存,第一个entry保存元素的value,第二个entry来保存元素的score。
当使用skiplist作为zset的底层存储结构时,使用skiplist按序保存元素value及score,使用dict来保存元素value和score的映射关系
skiplist
skiplist中,每个节点不单单只包含指向下一个节点的指针,可能包含多个指向后续节点的指针,这样在查找的时候就可以跳过一些不必要的节点。虽然单个节点存放多个指向后续节点的指针对于内存的开销增大,但是可以提升查找、删除的效率。
例如我们需要找到score为13的value,我们可以直接从头部level3的节点开始查,第一次查找就可以找到,跳过了2、5、9这些节点。
总结
redis常用的五中数据类型其实相对来说比较简单易用,了解其底层存储结构后,我们才能在各个场景选择最适用的数据类型去存储数据。五种数据类型底层都至少包含两种数据结构,一般第一种数据结构适用于数据量少,单个数据小的场景,例如:embstr、ziplist、intset,这些数据结构主要是为了节省内存空间,此时数据结构的查找、删除效率并不高,但是由于数据量少,单个数据小,所以并不会对redis读写效率有较大的影响。一般第二种数据结构适用于数据量多,单个数据大的场景,例如:raw、hashtable、skiplist,这写数据结构的优势在于读写效率高,但是对于内存的占用会更大,适合处理数据量多,单个数据大的场景。
redis版本更新较快,底层数据结构也有些变化,比如最新版的list底层已经使用quicklist来替代ziplist和linkedlist了,但是redis的核心思想是不会变的,数据量少、单个数据小时采用节省空间的策略,数据量多、单个数据大时采用提升读写效率额策略。