Redis 系列笔记:
第一篇:Redis 基础命令
第二篇:Redis 常见应用场景
第三篇:Redis Cluster集群搭建
第四篇:Redis 主从及哨兵搭建
第五篇:Redis 主从及集群
第六篇:Redis 持久化
第七篇:Redis 分布式锁
第八篇:Redis 底层数据存储结构
第九篇:Redis 面试常问问题
文章目录
前言
Redis的性能高的原因之一是它每种数据结构都是经过专门设计的,并都有一种或多种数据结构来支持,依赖这些灵活的数据结构,来提升读取和写入的性能,接下来一起了解一下它的数据存储原理。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Redis的数据是怎么存储的
Redis是一种存储key-value
的内存型数据库,它的key都是字符串类型,value支持存储5种类型的数据:String(字符串类型)、List(列表类型)、Hash(哈希表类型、即key-value类型)、Set(无序集合类型,元素不可重复)、Zset(有序集合类型,元素不可重复)。
从Redis内部实现的角度来看,key-value
这个映射关系是用一个 dict
来维护的。
1. hash算法
dict
使用两个 dictht
(哈希表)组成,当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出 哈希值
和索引值
, 然后再根据 索引值
, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
#define dictHashKey(d, key) (d)->type->hashFunction(key)
unsigned int h;
dictEntry *he;
h = dictHashKey(ht, key) & ht->sizemask;
he = ht->table[h];
2. hash冲突
如果两个不同的键值经过hash算法计算后,得到了相同的索引值
,则说明键冲突了。redis使用拉链法解决冲突。每个节点有一个next指针,新的键值对冲突时,则将新的键值对dictEntry放在单链表的首部(注意不是尾部)。
解决办法: 哈希表的每个节点都有一个 next 指针, 多个节点可以用 next 指针构成一个
单向链表
, 这就解决了键冲突的问题。为了执行效率没有尾节点指针,新节点将添加到链表的表头位置(复杂度为 O(1)), 排在其他已有节点的前面。
3. rehash
随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的 负载因子
维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
1、服务器目前没有在执行
bgsave
命令或者bgrewriteaof
命令、并且哈希表的负载因子大于等于1
2、服务器目前正在执行bgsave
命令或者bgrewriteaof
命令并且哈希表的负载因子大于等于5
rehash步骤:
1、重新为ht[1]分配空间,大小取决于ht[0].used的值。
- 扩容:ht[1]大小为(2n) * ht[0].used。
- 假设 当前的键值对数量为7,那么 7 * 2 = 14,首个大于等于14的数,并且是2的n次幂,这个数就是16 = 24,所以ht[1]的大小就是16。
- 收缩:ht[1]大小为2n,这个2n是第一个大于ht[0].used的2的n次幂。
2、将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面:
- rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
3、当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。
4. 渐进式rehash
上面的rehash是一次性、集中式地完成的,如果哈希表里保存的键值对数量很多的话,一次性将这些键值对全部 rehash 到 ht[1]上,庞大的计算量可能会导致服务器在一段时间内停止服务。
渐进式rehash
是分多次、渐进式地完成的,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上。这样就很巧妙地将一次性大量拷贝的开销,分摊到多次处理请求的过程中了,避免了耗时操作,保证了数据的快速访问。
渐进式rehash
步骤:
1、为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2、在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
3、在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
4、随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
在进行渐进式 rehash
的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash
进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找。
另外, 在渐进式 rehash
执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
5.dict结构图
dict的key固定用一种数据结构来表达就够了,这就是动态字符串SDS(simple dynamic string)
。而value则比较复杂,为了在同一个dict内能够存储不同类型的value,这就需要一个通用的数据结构,这个通用的数据结构就是robj(全名是redisObject)。
举个例子:如果value是一个list,那么它的内部存储结构是一个quicklist(quicklist的具体实现我们放在后面的文章讨论);如果value是一个string,那么它的内部存储结构一般情况下是一个sds。当然实际情况更复杂一点,比如一个string类型的value,如果它的值是一个数字,那么Redis内部还会把它转成long型来存储,从而减小内存使用。而一个robj既能表示一个sds,也能表示一个quicklist,甚至还能表示一个long型。
二、RedisObject对象解析
1. RedisObject结构
typedef struct redisObject{
//类型
unsigned type:4; //
//编码
unsigned encoding:4;
//对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS
//引用计数
int refcount
//指向底层实现数据结构的指针
void *ptr;
…..
}
type
: 对象的数据类型。占4个bit。可能的取值有5种(分别对应Redis对外暴露的5种数据结构):
type | 类型 |
---|---|
REDIS_STRING | 字符串 |
REDIS_LIST | 列表 |
REDIS_SET | 无序集合 |
REDIS_ZSET | 有序集合 |
REDIS_HASH | 哈希 |
encoding
: 对象的内部表示方式(也可以称为编码)。占4个bit。可能的取值有10种。
- OBJ_ENCODING_RAW: 最原生的表示方式。其实只有string类型才会用这个encoding值(表示成sds)。
编码常量(encoding ) | 编码对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long 类型的整数 |
REDIS_ENCODING_EMBSTR | embstr (编码的简单动态字符串,一种特殊的嵌入式的sds) |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_HT | dict(字典) |
REDIS_ENCODING_INTSET | 整数集合,用于set数据结构 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典,用于sorted set数据结构。 |
REDIS_ENCODING_QUICKLIST | 快速列表 |
REDIS_ENCODING_STREAM | 流 |
lru
: 做LRU替换算法用,占24个bit。这个不是我们这里讨论的重点,暂时忽略。
refcount
: 引用计数。它允许robj对象在某些情况下被共享。
ptr
: 数据指针。指向真正的数据。
- 比如,一个代表string的robj,它的ptr可能指向一个sds结构;一个代表list的robj,它的ptr可能指向一个quicklist。
我们来总结一下robj的作用:
- 为多种数据类型提供一种统一的表示方式。
- 允许同一类型的数据采用不同的内部表示,从而在某些情况下尽量节省内存。
- 支持对象共享和引用计数。当对象被共享的时候,只占用一份内存拷贝,进一步节省内存。
2. 不同数据类型存储方式
详细代码分析可参考B站视频学习。
每一种redisObject对象对应底层都会有至少2种数据结构
类型 | 编码 | 对象 |
---|---|---|
String | int | 整数值实现 |
String | embstr | sds实现 <=39 字节 |
String | raw | sds实现 > 39字节 |
Hash | ziplist | 压缩列表实现 |
Hash | hashtable | 字典使用 |
List | ziplist | 压缩列表实现 |
List | linkedlist | 双端链表实现 |
Set | intset | 整数集合使用 |
Set | hashtable | 字典实现 |
Sorted set | ziplist | 压缩列表实现 |
Sorted set | skiplist | 跳跃表和字典 |
2.1 String
String
的编码方式有三种:long
,embstr
,raw
。其中embstr
和 raw
都会使用SDS
来保存值,但不同之处在于embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存 redisObject
和 SDS
。
内部编码 | 条件 | 备注 |
---|---|---|
int | 满足long取值范围,也就是-9223372036854775808 ~ 9223372036854775807之间 | 如果设置字符串为数组类型操作long的范围,小于44字节。比如值为9223372036854775808则类型会变为embstr |
embstr | 非数组类型,若为数字。则不在long取值范围。且小于44字节。redis 3.2之前则小于39 | 如果大于44字节,则会变为raw类型,连续内存。注:redis3.2版本后,3.2版本中是39字节redis中embstr与raw编码方式之间的界限 |
raw | 大于44字节。redis3.2之后 | 满足等于或大于45字节,非连续内存。 |
为什么是44字节:
假设系统是64位,SDS中没有任何数据的情况下,emstr
中redisObject
就需要消耗的字节数就有16字节,sdshdr8
中的len
,alloc
,flags
各占一个字节,最后的buf的结束符’\0’所占用一字节,共16+3+1 = 20字节,剩余 64-20=44字节
,所以最终embstr能存储的字符串最大为44字节。
embstr和sds的区别在于内存的申请和回收:
- embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为redisObject分配对象,embstr省去了第一次)。相对地,释放内存的次数也由两次变为一次。
- embstr的redisObject和sds放在一起,更好地利用缓存带来的优势
- 缺点:redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改
1、int
2、embstr
3、raw
4、sds源码
String
类型的SDS
源码实现,redis3.2以后源码 :
// sdshdr5已经弃用,会自动转换成sdshdr8
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
// buf 存的字符串长度已占用的长度
uint8_t len; /* used */
// buf 中未被占用的长度
uint8_t alloc; /* excluding the header and null terminator */
// 标志位,用来表明这个是sdshdr几
unsigned char flags; /* 3 lsb of type, 5 unused bits */
// 存字符串的字符数组
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
SDS
的优点:
- 获取字符串长度复杂度O(1): 当查询字符串长度的时候,直接返回该字段的值,而不用通过遍历字符数组得到,查询时间复杂度为O(1)。设置和更新 SDS 长度的工作是由 SDS 的 API 在执行时自动完成的,使用 SDS 无须进行任何手动修改长度的工作。
**空间预分配:**当 SDS 的 API 对一个 SDS 进行修改,修改后的 SDS 的字符串小于1MB时(也就是 len 的长度小于1MB),那么程序会分配与len属性相同大小的未使用空间(就是再给未使用空间 alloc 也分配与 len 相同的空间);如果修改后SDS 大于1MB时(也就是len的长度大于等于1MB),那么程序会分配1MB的未使用空间。 【通过空间预分配操作,redis有效的减少了执行字符串增长所需要的内存分配次数】
- 例:字符串大小为600k,那么会分配600k给这个字符串使用,再分配600k的alloc空间在那。
- 例:字符串大小为3MB,那么会分配3MB给这个字符串使用,再分配1MB的alloc空间在那。
惰性空间释放: 当缩短 SDS 的存储内容时,并不会立即使用内存重分配来回收字符串缩短后的空间,而是通过alloc将空闲的空间记录起来,等待将来使用。真正需要释放内存的时候,通过调用api来释放内存。
- 释放 未使用空间(alloc) 的条件是,alloc > 10% * len,这样的目的就是为了防止多次 apend 这样的操作导致的多次 realloc调用。
注意
:redis3.2 之前的版本使用字段叫做free
,表示未使用空间长度;而 redis3.2 以及之后的版本已经改成了alloc
字段,表示分配的总空间大小`
参考: redis之SDS字符串,到底高效在哪里?(全面分析)
2.2 Hash
Hash
底层实现采用了 ziplist
和 hashtable
两种实现方式。ziplist
(压缩链表) 适用于长度较小的值,因为他是由连续的空间实现的。存取的效率高,内存占用小,但由于内存是连续的,在修改的时候要重新分配内存。在数据量比较小的时候使用的是 ziplist
。当 hash
对象同时满足以下两个条件是,使用的 ziplist
编码。
内部编码 | 条件 | 备注 |
---|---|---|
zipList | hash 中存储的所有元素的 key 和 value 的长度都小于 64byte hash 中存储的元素个数小于 512 | 通过修改 hash-max-ziplist-value 配置调节大小 通过修改 hash-max-ziplist-entries 配置调节大小 |
hashtable | 上面两个条件一旦有一个条件不满足时就会被转码为 hashtable进行存储 | 在不满足条件时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。 |
1、ziplist
由于压缩列表基于内存特殊编码实现,源码种并没有数据结构代码定义,但可以通过上文的宏定义函数,结合压缩列表的创建函数来推出压缩列表的大致组成部分。ziplist
创建函数:
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
// 为压缩列表申请bytes字节空间
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
// 默认小端模式存储 如果在使用大端存储的机器上运行 需要大小端转换 详见计组
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
// 设置len属性为0 len表示ziplist中节点数量
ZIPLIST_LENGTH(zl) = 0;
// 设置ziplist结尾标志0xFF
zl[bytes-1] = ZIP_END;
return zl;
}
ziplist中列表节点定义:
typedef struct zlentry {
// 用于记录内存编码后前一个entry的len占用了多少字节 即prevrawlen占用了多少字节
unsigned int prevrawlensize;
// 用于记录前一个entry占用的长度
// 通过当前entry地址 - prevrawlen可以寻找到上一个entry的首地址
unsigned int prevrawlen;
// 用于记录内存编码后当前entry的len占用了多少字节
unsigned int lensize;
// 用于记录当前entry的长度
unsigned int len;
// 用于记录当前entry的header大小 即lensize + prevrawsize
unsigned int headersize;
// 用于记录当前节点编码格式
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
ziplist
中 entry
参数解析:
len
:
- 当前一个节点长度小于254字节时,直接将前一节点存储在当前字节中
- 当前一个节点长度大于等于254字节时,将prevrawlen的第一个字节设为254,后四个字节用于保存前一节点长度(因为unsigned int固定4字节)。
encoding
: entry
被设置允许存放字符串和整数类型数据,由于压缩列表的主要目的是尽量节省空间,所以字符串和整数类型的数据有也自然有他的压缩(编码)方式。存储不同类型不同长度的数据所选用的编码方式也随之改变。
字符串编码:
编码 | 编码长度 | 实际存储的数据 |
---|---|---|
00xxxxxx | 1字节 | 长度小于等于63字节的字节数组 |
01xxxxxx xxxxxxxx | 2字节 | 长度小于等于16383字节的字节数组 |
10_ _ _ _ _ _ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx | 5字节 | 长度小于4294 967 295字节的字节数组 |
整数编码:
编码 | 编码长度 | 实际存储的数据 |
---|---|---|
11000000 | 1字节 | int16_t类型的整数 |
11010000 | 1字节 | int32_t类型的整数 |
11100000 | 1字节 | int64_t类型的整数 |
11110000 | 1字节 | 24位有符号整数 |
11111110 | 1字节 | 8位有符好整数 |
1111xxxx | 1字节 | encoding的低四位直接保存一个0~12的整数 |
参考:Redis 5.0数据结构之压缩列表ziplist源码详解
2、hashtable
dict 字典的底层就是通过hashtable
来实现的。
源码:
/*
* dict 字典
* 大家需要关注的是dictht ht[2]:
* 这里设计存储两个dictht 的指针是用于Redis的rehash,后文中进行详解
*/
typedef struct dict {
dictType *type; /*类型特定函数*/
void *privdata; /*私有数据*/
dictht ht[2]; /*用于存储数据的两个hash表,正常只有一个hash表中有数据,只有在rehash的过程中才会出现两个hash表同时存在数据*/
long rehashidx; /*rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1*/
unsigned long iterators; /*迭代器数量*/
} dict;
/*
* 这是我们的哈希表结构。 每个字典都有两个
* 一个哈希表里面有多个哈希表节点(dictEntry),每个节点表示字典的一个键值对
*/
typedef struct dictht {
dictEntry **table; /*哈希表数组指针*/
unsigned long size; /*hashtable 容量 数组大小*/
unsigned long sizemask; /*size -1*/
unsigned long used; /*hashtable中元素个数,正常情况下当used/size=1时将进行扩容操作*/
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
void *key;
union {
void *val; /*指向Value值的指针,正常是指向一个redisObject*/
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; /*当出现hash冲突时使用链表形式保存hashcode相等但是field 不相等的数据,这里就是指向下一条数据的指针*/
} dictEntry;
注:
这里的dict具体结构是上面那种还是其他结构还不清楚,欢迎留言补充。
2.3 List
List
列表类型的内部编码3.2 版本之前有两种:ziplist
(压缩列表) 和 linkedlist
(双向链表),3.2 版本之后,重新引入了一个 quicklist
(快速列表) 的数据结构,List
底层都由 quicklist
实现。
内部编码 | 条件 | 备注 |
---|---|---|
ziplist | Hash 中存储的所有元素的长度都小于 64byte,且存储的元素个数小于 512 | 通过修改 hash-max-ziplist-entries,hash-max-ziplist-entries配置调节大小 |
linkedlist | 当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。 | 双向链表便于在表的两端进行 push 和 pop 操作,在插入节点上复杂度很低,但是它的内存开销比较大。 首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。 |
quicklist | zipList和linkedList的混合体,是将linkedList按段切分,每一段用zipList来紧凑存储,多个zipList之间使用双向指针链接。 | quicklist 其实就是综合考虑了时间和空间效率引入的新型数据结构。(使用 ziplist 能提高空间的使用率,使用 linkedlist 能够降低插入元素时的时间) |
quicklist 极端情况:
- 1、当 ziplist 节点过多的时候,quicklist 就会退化为双向链表。效率较差;效率最差时,一个 ziplist 中只包含一个 entry,即只有一个元素的双向链表。(增加了查询的时间复杂度)
- 2、当 ziplist 元素个数过少时,quicklist 就会退化成为 ziplist,最极端的时候,就是 quicklist 中只有一个 ziplist 节点。(当增加数据时,ziplist 需要重新分配空间)
1、ziplist
2、linkedlist
在使用redis的list数据结构时,存储数据较大时,list对象已经不满足上面描述的ziplist条件,则会使用linkedlist:
- 优点:修改效率高
- 缺点:保存前后指针,会占内存。
双端列表结构源码:
typedef struct list {
// 头节点
listNode *head;
// 尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所包含节点数量
unsigned long len;
} list;
列表中节点源码:
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 值
void *value;
} listNode;
结构图:
3、quicklist
结构源码:
typedef struct quicklist {
quicklistNode *head; /* 指向双向链表的表头 */
quicklistNode *tail; /* 指向双向链表的表头 */
unsigned long count; /* 所有的ziplist中一共存了多少个元素,即ziplist中的entry个数 */ /* total count of all entries in all ziplists */
unsigned long len; /* 双向链表的长度,quicklistNode的数量 */ /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* ziplist最大大小,对应list-max-ziplist-size */ /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* 压缩深度,对应list-compress-depth */ /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS; /* 4位,bookmarks数组的大小 */
quicklistBookmark bookmarks[]; /* bookmarks是一个可选字段,quicklist重新分配内存空间时使用,不使用时不占用空间 */
} quicklist;
快速列表节点源码:
typedef struct quicklistNode {
struct quicklistNode *prev; /* 指向前一个节点 */
struct quicklistNode *next; /* 指向后一个节点 */
unsigned char *zl; /* 指向实际的ziplist */
unsigned int sz; /* 当前ziplist占用多少字节 */ /* ziplist size in bytes */
unsigned int count : 16; /* 当前ziplist中存储了多少个元素,占16bit(下同),最大65536个 */ /* count of items in ziplist */
unsigned int encoding : 2; /* 是否采用了LZF压缩算法压缩节点 */ /* RAW==1 or LZF==2 */
unsigned int container : 2; /* 2:ziplist,未来可能支持其它存储结构 */ /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* 当前ziplist是否已经被解压出来作临时使用 */ /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* 测试用 */ /* node can't compress; too small */
unsigned int extra : 10; /* 预留给未来使用 */ /* more bits to steal for future usage */
} quicklistNode;
结构图:
quicklist 的特点:
1.是一个节点为 ziplist 的双端链表
2.节点采用了 ziplist ,解决了传统链表的内存占用问题
3.能控制 ziplist 的大小,解决连续内存空间申请的效率问题
4.中间节点可以进行压缩,进一步节省了内存空间
2.4 Set
set
的内部编码有 intset
(整数集合)整数和 hashtable
(哈希表) 两种实现方式:
内部编码 | 条件 | 备注 |
---|---|---|
intset | 当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时 | |
hashtable | 当集合类型无法满足 intset 的条件时,会使用 hashtable 作为集合的内部实现 |
1、intset
intset
内部其实是一个数组(int8_t coentents[]数组),而且存储数据的时候是有序的,因为在查找数据的时候是通过二分查找来实现的。
结构源码:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents 虽然声明为 int8_t 类型,但它实际上并不保存任何 int8_t 类型的值, contents 数组的实际类型取决于 encoding 属性的值:
编码 | encoding值 | 备注 |
---|---|---|
int16_t | INTSET_ENC_INT_6 | contents存储 int16_t 类型的值 |
int32_t | INTSET_ENC_INT_6 | contents存储 int16_t 类型的值 |
int64_t | INTSET_ENC_INT_6 | contents存储 int16_t 类型的值 |
结构图:
2、hashtable
2.5 Sorted set
set
的内部编码有 ziplist
(压缩列表)整数和 skiplist
(跳跃表) 两种实现方式:
内部编码 | 条件 | 备注 |
---|---|---|
ziplist | 当有序集合中所有元素的 key 和 value 的长度都小于 64byte Hash 中存储的元素个数小于 512 | 配置项参考上面 hash 介绍 |
skiplist | 当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时zip的读写效率会下降 |
1、ziplist
2、skiplist
源码:
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; //成员对象
double score; //分值
struct zskiplistNode *backward; //后退指针
//层
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针
unsigned long span; //跨度
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //头结点,尾节点
unsigned long length; //跳表长度
int level; //当前最大层高
} zskiplist;
typedef struct zset {
dict *dict; //跳表中的所有键值对
zskiplist *zsl;
} zset;
结构图:
跳表原理可参考:Redis源码解析:数据结构详解-skiplist
总结
以上我根据源码和图片简单讲解了一下redis的5种数据类型的底层结构,还有一些对数据的修改操作没有讲到,想要具体了解就去看看源码。后面还会陆续补充一些内容。如:redis的rehash
、渐进式hash
。