Redis 底层有6种数据结构
使用SDS存储字符串对象
简单动态字符串(simpledynamicstring,SDS),用作Redis的默认字符串表示。
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
SDS遵循C字符串以空字符结尾的惯例,以便可以直接重用一部分C字符串函数库里面的函数,如:
printf("%s",s->buf);
SDS的优势
-
常数复杂度获取字符串的长度。
C字符串不记录其长度,获取长度时必须遍历每个字符直到
\0
,其时间复杂度为 。SDS中记录了长度,获取字符串长度的时间复杂度仅为 。
即使对一个非常长的字符串键反复执行
STRLEN
命令,也不会对系统性能造成任何影响。 -
杜绝缓冲区溢出。
C字符串在进行字符串拼接时,可能由于没有足够的空间导致缓冲区溢出,污染其他字符串的值。
SDS在执行类似的操作前,会检查并分配空间,保证操作的安全。
-
减少修改字符串时带来的内存重分配次数。
C字符串在每次增长或缩短时,为了保证存储字符串的字符数组的长度是 ,都要进行内存重分配,比较耗时。
SDS解除了字符串长度与底层数组长度之间的约束,通过空间预分配和惰性空间释放策略,优化未使用的空间,使字符串的变动更为高效。
-
二进制安全。
C字符串不能包含空字符
\0
,否则会被认为是结尾。因此不能存储图片、多媒体、压缩文件等二进制数据。SDS的API都是二进制安全的,程序不会对数据做任何限制、过滤或假设。因此
buf
属性又称为字节数组,而不是字符数组
双向链表
链表可以高效的重排节点,灵活的增删节点调整长度。Redis中,主要应用于链表键、发布与订阅、慢查询、监视器等功能。
双向链表的数据结构
typedef struct list {
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数,用于复制链表节点所保存的值
void *(*dup)(void *ptr);
//节点值释放函数,用于释放节点所保存的值
void (*free)(void *ptr);
//节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等
int(*match)(void *ptr,void *key);
} list;
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
} listNode;
- 链表节点的值使用
void *
类型,并且可以使用dup
、free
、match
设置特定类型的函数,所以链表可以保存不同类型的值。 - 链表的表头节点的前置节点和表尾节点的后置节点都指向
NULL
,所以Redis的链表实现是无环链表。
字典
Redis中,字典用于保存数据库中的键值对,对数据库的增删改查就是构建在对字典的操作之上。
字典的数据结构
typedef struct dict {
// 类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为1
int rehashidx;
/*rehashing not in progress if rehashidx==1 */
} dict;
- ht属性包含两个元素,一般只用第一个元素,第二个元素用于rehash
dictType
结构,用于对特定类型键值对进行操作:
typedef struct dictType {
//计算哈希值的函数
unsigned int(*hashFunction)(const void *key);
//复制键的函数
void*(*keyDup)(void* privdata, const void *key);
//复制值的函数
void*(*valDup)(void *privdata, const void *obj);
//对比键的函数
int(*keyCompare)(void *privdata, const void *key1, const void *key2);
//销毁键的函数
void(*keyDestructor)(void *privdata, void *key);
//销毁值的函数
void(*valDestructor)(void *privdata, void *obj);
} dictType
哈希表数据结构:
typedef struct dictht {
//哈希表数组,每个元素都是一个指向`dictEntity`结构的指针
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
typedef struct dictEntity {
// 保存键名
void *key;
// 保存键值,可以是指针或整数。
union {
void *val;
uint64_tu64;
int64_ts64;
} v;
// 指向下个哈希表节点,用于将多个哈希值相同的键值对连接起来
struct dictEntity *next;
} dictEntity
next
解决键冲突问题。
压缩列表
类似于一个数组,数组中的每一个元素都对应保存一个数据。
压缩列表在表头有三个字段 zlbytes
(列表长度)、zltail
(列表尾的偏移量) 和 zllen
(列表中的 entry 个数);压缩列表在表尾还有一个 zlend
,表示列表结束。
跳跃表
跳跃表(Skip List)是一个有序的数据结构,用于实现有序集合键,以及用于集群节点中的内部数据结构。
跳跃表的数据结构
typedef struct zskiplist{
//表头节点和表尾节点
struct zskiplistNode *header, *tail;
//表中节点的数量(不含表头节点)
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
typedef struct zskiplistNode {
//层
struct zskiplistLevel{
//前进指针,用于访问位于表尾方向的其他节点
struct zskiplistNode *forward;
//跨度,表示前进指针指向的节点与当前节点的距离
unsigned int span;
}level[];
//后退指针,从表尾向表头遍历时使用,指向当前节点的前一个节点
struct zskiplistNode *backward;
//分值,即排序值,排序方式为顺序
double score;
//成员对象
robj *obj;
} zskiplistNode
整数数组
双向链表
Redis使用了一个哈希表来保存所有键值对
键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。
哈希表中的哈希桶存放值的指针
- 哈希表的每个元素称为一个哈希桶;
- 一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据;
- 哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。
哈希桶中的 entry
元素(底层使用字典实现)中保存了*key
和*value
指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value
指针被查找到。
哈希表最大的好处是访问速度快
-
查找键值对的时间复杂度是
O(1)
我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的
entry
元素。
哈希表存在冲突以及rehash问题
往 Redis 中写入大量的数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在的风险点,那就是哈希表的冲突问题和 rehash
可能带来的操作阻塞。
哈希冲突
哈希桶的个数通常要少于 key 的数量,这也就是说,难免会有一些 key 的哈希值对应到了同一个哈希桶中。
链式哈希解决冲突
链式哈希指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
entry1
、entry2
和entry3
都需要保存在哈希桶 3 中,导致了哈希冲突。- 通过一个
*next
指针将多个元素串联起来
通过 rehash 增加哈希桶的数量
Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
-
使用了两个全局哈希表;
-
首先使用哈希表1;
此时的哈希表 2 并没有被分配空间
-
随着数据逐步增多,给哈希表 2 分配更大的空间;
-
把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
如果数据量太大,会导致阻塞,所以采用渐进式 rehash:从哈希表 1 中的第一个索引位置开始,
顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。 -
释放哈希表 1 的空间,为下次扩容使用。