Redis的大部分操作都是对于键值对的增删改查,想要实现快速的查找并且操作键值对,底层的数据结构尤为重要。
我们平常所熟悉的String、List、Set、Sorted Set、Hash等只是Redis键值对中值的数据类型,也就是数据的保存类型,而他们底层的实现是如何的呢?
Redis中Key和value是如何组织的?
Redis使用哈希表保存所有的key和value,目的是为了从key到value的快速访问。
哈希表中的value保存的并非值得本身,而是值对应的指针地址。
为什么使用哈希表?
时间复杂度为O(1),只需要计算key的哈希值即可找到对应的value指针。key的哈希值的计算和数据量的大小没有关系,也就说不管哈希表里面有1万key和1000万的key,只需要计算一次key的哈希值即可找到对应value的指针。
哈希表的问题
哈希冲突问题(不同的key相同的哈希值)和rehash问题带来的操作阻塞。
具体的表现,当你往Redis写入大量的数据时,突然发现响应变慢了。
如何解决哈希冲突问题?
使用链式哈希,同一个哈希桶中的多个元素间用链表保存,元素间通过指针关联。如此一来,链表上的数据还是需要逐一查找后才能操作,意思是链表越长,查询和操作对应的耗时也就越长问题,如何解决呢?使用rehash(增加哈希桶的数量)。
rehash实现。
采用渐进式rehash方式(分而治之的方式),避免rehash时大量可以的拷贝导致的操作阻塞。
什么是渐进式rehash?
Cluster模式下:
1.一个Redis实例对应一个RedisDB(db0);
2.一个RedisDB对应一个Dict;
3.一个Dict对应2个Dictht,正常情况只用到ht[0];ht[1] 在Rehash时使用。
/* Redis数据库结构体 */
typedef struct redisDb {
// 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
dict *dict;
// 键的过期时间
dict *expires;
// 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
dict *blocking_keys;
// 准备好数据可以解除阻塞状态的键和相应的client
dict *ready_keys;
// 被watch命令监控的key和相应client
dict *watched_keys;
// 数据库ID标识
int id;
// 数据库内所有键的平均TTL(生存时间)
long long avg_ttl;
} redisDb;
/* 字典结构定义 */
typedef struct dict {
dictType *type; // 字典类型
void *privdata; // 私有数据
dictht ht[2]; // 哈希表[两个](定义了两张哈希表,是为了后续字典的扩展作Rehash之用)
long rehashidx; // 记录rehash 进度的标志,值为-1表示rehash未进行
int iterators; // 当前正在迭代的迭代器数
} dict;
Redis通过dictCreate()创建词典,在初始化中,table指针为Null,所以两个哈希表ht[0].table和ht[1].table都未真正分配内存空间。只有在dictExpand()字典扩展时才给table分配指向dictEntry的内存。
rehash步骤:
1.为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2.将rehashindex的值设置为0,表示rehash工作正式开始
3.在rehash期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashindex索引上的所有键值对rehash到ht[1],当rehash工作完成以后,rehashindex的值+1
4.随着字典操作的不断执行,最终会在某一时间段上ht[0]的所有键值对都会被rehash到ht[1],这时将rehashindex的值设置为-1,表示rehash操作结束
rehash触发条件是什么?
1.扩容情况1:当前redis没有进行BGWRITEAOF或者BGSAVE命令,哈希表已使用节点数量/哈希表大小>=1,并且dict_can_resize=1(dict_can_resize指示字典是否启用 rehash 的标识)
2.扩容情况2:哈希表已使用的节点数量/哈希表大小>=5,无论是否在进行BGWRITEAOF或者BGSAVE命令,都会进行扩容(rehash)。
3.缩容情况1:哈希表已使用节点数量/哈希表大小< 0.1,dict就会触发缩减操作rehash。
注:已使用节点数量可理解为key数量
rehash扩容后哈希表大小为多少?
4×2^n >= ht[0].used*2的值作为字典扩展的size大小。
4是哈希表默认大小值。
static unsigned long _dictNextPower(unsigned long size) {
unsigned long i = DICT_HT_INITIAL_SIZE; // 哈希表的初始值:4
if (size >= LONG_MAX) return LONG_MAX;
/* 计算新哈希表的大小:第一个大于等于size的2的N 次方的数值 */
while(1) {
if (i >= size)
return i;
i *= 2;
}
}
rehash时key查询如何执行?
根据ht[0]计算key哈希值,判断是否存在,如果存在则返回;如果不存在根据ht[1]计算key哈希值,判断是否存在,如果存在则返回。
rehash时增删改如何执行?
根据ht[0]计算key哈希值,判断是否存在,如果存在则执行操作;如果不存在根据ht[1]计算key哈希值,判断是否存在,如果存在则执行操作,如果不存在则增加或者删除。
新key的操作在ht[1]执行。
注:图片来自美团技术团队
详细可以阅读:
Redis 高负载下的中断优化
美团针对Redis Rehash机制的探索和实践
6种底层数据结构
每种数据结构特性不一样,操作时间也不一样。
数据类型和底层数据结构对应关系。
1.简单动态字符串
Sds (Simple Dynamic String,简单动态字符串)。可以参考另外一篇文章“Redis核心技术-数据结构3-String”。
2.双向链表
/* list节点*/
typedef struct listNode {
struct listNode *prev; // 前向指针
struct listNode *next; // 后向指针
void *value; // 当前节点值
} listNode;
/*链表结构*/
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;
3.压缩列表
压缩列表(ziplist),Redis为了节约内存而开发的。
ziplist包括zip header、zip entry、zip end三个模块。
1.zip entry由prevlen、encoding&length、value三部分组成。
2.prevlen主要是指前面zipEntry的长度,coding&length是指编码字段长度和实际- 存储value的长- 度,value是指真正的内容。
3.每个key/value存储结果中key用一个zipEntry存储,value用一个zipEntry存储。
注:图片来自“压缩列表”
typedef struct ziplist{
/*ziplist分配的内存大小*/
uint32_t bytes;
/*达到尾部的偏移量*/
uint32_t tail_offset;
/*存储元素实体个数*/
uint16_t length;
/*存储内容实体元素*/
unsigned char* content[];
/*尾部标识*/
unsigned char end;
}ziplist;
/*元素实体所有信息, 仅仅是描述使用, 内存中并非如此存储*/
typedef struct zlentry {
/*前一个元素长度需要空间和前一个元素长度*/
unsigned int prevrawlensize, prevrawlen;
/*元素长度需要空间和元素长度*/
unsigned int lensize, len;
/*头部长度即prevrawlensize + lensize*/
unsigned int headersize;
/*元素内容编码*/
unsigned char encoding;
/*元素实际内容*/
unsigned char *p;
}zlentry;
4.哈希表
Redis Hash 类型的两种底层实现结构,分别是压缩列表和哈希表。
Hash 类型底层结构什么时候使用压缩列表,什么时候使用哈希表呢?
Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。
这两个阈值分别对应以下两个配置项:
1.hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
2.hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
在redis.conf中配置,默认值如下:
#元素数量小于512
hash-max-ziplist-entries 512
#字符串长度都小于64字节
hash-max-ziplist-value 64
压缩列表转为了哈希表后,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。在节省内存空间方面,哈希表就没有压缩列表那么高效了。
5.跳表
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
6.整数数组
整数集合(intset)并不是一个基础的数据结构,是Redis自己设计的一种存储结构,是集合键的底层实现之一。当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
//每个intset结构表示一个整数集合
typedef struct intset{
//编码方式
uint32_t encoding;
//集合中包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。
参考:
https://blog.csdn.net/mccand1234/article/details/93411326
https://www.cnblogs.com/hunternet/p/11306690.html
https://www.cnblogs.com/hunternet/p/11248192.html
https://www.cnblogs.com/hunternet/p/11268067.html(Redis数据结构——整数集合)
https://mp.weixin.qq.com/s/7ct-mvSIaT3o4-tsMaKRWA
注:部分图片来自极客时间