我们都只知道,在我们的Redis中,最常用的Value数据结构有5个,string、list、hash、set、zset,但是这五个在内存中的存储还是有一些变化的,接下来逐个的介绍一下这几个
1、string
对于string形式,我们Redis底层在内存中的存储是由三种结构的,这三种结构分别是int,embstr,raw。
在上一篇博客中我们讲到了真正存储Value的那个数据结构有两个属性,一个是type,一个是encoding,这个type是我们外部的属性,都是string,而这个encoding就是内存中的编码,虽然type都是string,但它的数据类型却有更细的分类,就是int,embstr,raw。
如下图所示:
type str
object encoding
1.1、int
当我们set一个整型值的时候,Redis内存中的编码结构就是int类型。
1.2、embstr
当我们set一个比较短的字符串的时候,Redis内存中的编码结构就是embstr类型。
1.3、raw
当我们set一个比较长的字符串的时候,Redis内存中的编码结构就是raw类型。
这个字符串的长度到底有多长,这个和我们CPU的缓存行有关系,在我们这64位的系统中,一般缓存行是64字节,然而我们需要表示一个sds字符串的话,其它的属性需要20个字节,如下所示:
主要来自于redisObject属性以及sds属性:
就我们第二篇博客的最后两个数据结构,如下图所示:
redisObject总共占16个字节
// type 占 4bit
type
// encoding 占 4bit
encoding
// lru 占 3byte
lru
// refcount 占 4byte
refcount
// *ptr 占 8byte
*ptr
sds总共占4个字节
// 最小的 len 占 1byte
len
// free 占 1byte
alloc
// flags 占 1byte
flags
// \0 占 1byte
buf[]
所以当超过44个byte位之后,就会存储成raw类型,如下图所示:
2、list
Redis底层的list存储是双端链表(quicklist)和 ziplist 作为List的底层实现, quicklist 是外层实现 ziplist 是内层实现,如下图所示:
2.1、quicklist
在下面这张图中,我们可以很明显的看出 quicklist 是外层,ziplist 是内层, quicklist 专注于整个list的结构,ziplist 主要存储数据
2.2、ziplist
从这张图中我们就可以看出我们将list的数据分成了很多份,每个ziplist 存储其中的一份。
2.3、list压缩和数据量过大ziplist分裂的配置
配置 | 含义 |
---|---|
list-max-ziplist-size -2 | 单个ziplist节点最大能存储 8kb ,超过则进行分裂,将数据存储在新的ziplist节点中 |
list-compress-depth 1 | 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 … 以此类推 |
其实这些属性的说明也可以在redis.conf配置文件种的注释中看到,如下所示:
2.4、list为什么不用双向链表作为底层数据结构
因为list可以从前往后访问,也可以从后往前访问,如果说用链表的话每个节点就需要两个指针,每个指针占8个字节,一个节点的两个指针就占16个字节,所以就出现了下面的第一个问题:
1、浪费了大量的内存空间。
2、由于链表的内存空间不是连续的,所以会产生大量的内存碎片。
3、hash
在Redis底层对hash的存储也分为两种,一种是ziplist,还有一种是hashTable。
3.1、ziplist
当数据量比较小的时候,hash会存储为ziplist的形式,如下图所示:
3.2、hashTable
当元素过多或者单个元素数据过大时,Redis底层就会把hash的存储形式从 ziplist 转换成 hashTable ,这样避免了 ziplist 的访问效率问题。
3.3、从 ziplist 转换为 hashTable 的条件
在redis.conf配置文件中可以修改以下两个配置,即可改变在什么情况下,数据结构就由 ziplist 转换为 hashTable 。
配置 | 含义 |
---|---|
hash-max-ziplist-entries 512 | ziplist 元素个数超过 512 ,将改为hashtable编码 |
hash-max-ziplist-value 64 | 单个元素大小超过 64 byte时,将改为hashtable编码 |
3.4、Redis的渐进式ReHash
如果说一个位桶数组,某一个下标后面的数据非常非常多,而其它下标后面的数据却非常少,那么这个时候Redis就会进行ReHash。
ReHash的意思是会开辟一个为原数组2倍容量的数组,然后对原有数组的所有数组全部进行hash重新存储,但是为了防止数据量过大,全部一次性ReHash的话会影响客户端的操作等,所以就产生了渐进式的Rehash,这样客户端的访问也就不会卡顿了。
渐进式ReHash中如果写入了数据,会去先判断这个数据在老的hashTable中有没有,如果有,则将旧的修改之后搬到新的HashTable中,如果没有,从新的HashTable里面去找,找到则修改,找不到则直接写入到新的HashTable中。
4、set
Set 为无序的,自动去重的集合数据类型,底层也是dict,只可以存储一个null值,并且会自动帮我们去重,有两种存储结构,intset和hashTable。
4.1、intset
intset其实就相当于是一个数组,在set的元素全部都是在范围内的整型值,set底层就会用intset来存储。
4.1.1、intset的数据结构
数据结构如下:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
在上面代码中也可以看出,对于整型值的大小我们也分为了三种,可以用三种不同的数据结构进行存储,目的也就是为了节省空间,当某一个整型值超过了最大的范围,也就是 264-1,那么就得转换成hashTable的存储形式了。
4.1.2、intset示例
注意:intset还会自动排序。
从上图就可以看出,我们写入了9个元素,但是写成功了只有7个,去重了两个,然后我们遍历元素时,发现确实给我们排序了,再查看底层编码结构,用的是intset。
4.2、hashTable
我们有以下几个条件会使底层的数据结构从intset编程hashTable
1、单个整型值数据过大,大于 264-1。
2、数据的数量过多。
3、传入了一个数据不能转换为整型数据。
如下图所示:
4.3、从 intset 转换为 hashTable 的条件
修改下面的配置,即可改变数量。
配置 | 含义 |
---|---|
set-max-intset-entries 512 | intset 能存储的最大元素个数,超过则用hashtable编码 |
5、zset
zset 是一种有序的set集合,也会自动去重,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储,如下面代码:
typedef struct zset {
// 跳表
zskiplist *zsl;
// 字典
dict *dict;
} zset;
5.1、ziplist
对于 zset 的 ziplist 数据结构,其实就是将每一个元素的分值与value值分别存储,用两个entry元素来存储,数组嘛,肯定比链表要节省开销,如下图所示:
5.2、dict
zset的字典主要维护了元素的Value以及分值,这样就直接可以通过ZSCORE命令由O(1)的时间复杂度拿出分值。
5.3、zskiplist
zskiplist的大致模型如下图所示:
通过上图我们就可以看出,要查找一个数据时,时间复杂度是O(logN),和我们的二分法查询效率大致一样,里面的level是通过一个zslRandomLevel()函数生成的。
5.4、从 ziplist 转换为 字典(dict) + 跳表(skiplist) 的条件
配置如下参数即可
配置 | 含义 |
---|---|
zset-max-ziplist-entries 128 | 元素个数超过128 ,将用skiplist编码 |
zset-max-ziplist-value 64 | 单个元素大小超过 64 byte, 将用 skiplist编码 |
10、辅助知识
10.1、对于 2n 的取模
在Java中,对于 2n 取模可以转换成对 2n-1 做&运算,如下代码所示:
int X = NUM % 2^n^
int Y = NUM & ( 2^n^ - 1 )
X 和 Y 的结果是一样的。