说到Redis的数据结构,咱们大概会很快想到Redis的5种常见数据结构:字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set),以及他们的特色和运用场景。不过它们是Redis对外暴露的数据结构,用于API的操做,而组成它们的底层基础数据结构又是什么呢git
Redis的GitHub地址github.com/antirez/red…github
Redis是用C语言写的,可是Redis并无使用C的字符串表示(C是字符串是以\0
空字符结尾的字符数组),而是本身构建了一种简单动态字符串(simple dynamic string,SDS)的抽象类型,并做为Redis的默认字符串表示redis
在Redis中,包含字符串值的键值对底层都是用SDS实现的算法
SDS的结构定义在sds.h
文件中,SDS的定义在Redis 3.2版本以后有一些改变,由一种数据结构变成了5种数据结构,会根据SDS存储的内容长度来选择不一样的结构,以达到节省内存的效果,具体的结构定义,咱们看如下代码数据库
// 3.0
struct sdshdr {
// 记录buf数组中已使用字节的数量,即SDS所保存字符串的长度
unsigned int len;
// 记录buf数据中未使用的字节数量
unsigned int free;
// 字节数组,用于保存字符串
char buf[];
};
// 3.2
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
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[];
};
复制代码复制代码
3.2版本以后,会根据字符串的长度来选择对应的数据结构编程
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5) // 32
return SDS_TYPE_5;
if (string_size < 1<<8) // 256
return SDS_TYPE_8;
if (string_size < 1<<16) // 65536 64k
return SDS_TYPE_16;
if (string_size < 1ll<<32) // 4294967296 4G
return SDS_TYPE_32;
return SDS_TYPE_64;
}
复制代码复制代码
下面以3.2版本的sdshdr8
看一个示例数组
len
:记录当前已使用的字节数(不包括'\0'
),获取SDS长度的复杂度为O(1)alloc
:记录当前字节数组总共分配的字节数量(不包括'\0'
)flags
:标记当前字节数组的属性,是sdshdr8
仍是sdshdr16
等,flags值的定义能够看下面代码buf
:字节数组,用于保存字符串,包括结尾空白字符'\0'
// flags值定义
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
复制代码复制代码
上面的字节数组的空白处表示未使用空间,是Redis优化的空间策略,给字符串的操做留有余地,保证安全提升效率缓存
C语言使用长度为N+1的字符数组来表示长度为N的字符串,字符数组的最后一个元素为空字符'\0'
,可是这种简单的字符串表示方法并不能知足Redis对于字符串在安全性、效率以及功能方面的要求,那么使用SDS,会有哪些好处呢安全
参考于《Redis设计与实现》bash
常数复杂度获取字符串长度
C字符串不记录字符串长度,获取长度必须遍历整个字符串,复杂度为O(N);而SDS结构中自己就有记录字符串长度的len
属性,全部复杂度为O(1)。Redis将获取字符串长度所需的复杂度从O(N)降到了O(1),确保获取字符串长度的工做不会成为Redis的性能瓶颈
杜绝缓冲区溢出,减小修改字符串时带来的内存重分配次数
C字符串不记录自身的长度,每次增加或缩短一个字符串,都要对底层的字符数组进行一次内存重分配操做。若是是拼接append操做以前没有经过内存重分配来扩展底层数据的空间大小,就会产生缓存区溢出;若是是截断trim操做以后没有经过内存重分配来释放再也不使用的空间,就会产生内存泄漏
而SDS经过未使用空间解除了字符串长度和底层数据长度的关联,3.0版本是用free
属性记录未使用空间,3.2版本则是alloc
属性记录总的分配字节数量。经过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化的空间分配策略,解决了字符串拼接和截取的空间问题
二进制安全
C字符串中的字符必须符合某种编码,除了字符串的末尾,字符串里面是不能包含空字符的,不然会被认为是字符串结尾,这些限制了C字符串只能保存文本数据,而不能保存像图片这样的二进制数据
而SDS的API都会以处理二进制的方式来处理存放在buf
数组里的数据,不会对里面的数据作任何的限制。SDS使用len
属性的值来判断字符串是否结束,而不是空字符
兼容部分C字符串函数
虽然SDS的API是二进制安全的,但仍是像C字符串同样以空字符结尾,目的是为了让保存文本数据的SDS能够重用一部分C字符串的函数
C字符串与SDS对比
C字符串 | SDS |
---|---|
获取字符串长度复杂度为O(N) | 获取字符串长度复杂度为O(1) |
API是不安全的,可能会形成缓冲区溢出 | API是安全的,不会形成缓冲区溢出 |
修改字符串长度必然会须要执行内存重分配 | 修改字符串长度N次最多会须要执行N次内存重分配 |
只能保存文本数据 | 能够保存文本或二进制数据 |
可使用全部<string.h> 库中的函数 |
可使用一部分<string.h> 库中的函数 |
链表是一种比较常见的数据结构了,特色是易于插入和删除、内存利用率高、且能够灵活调整链表长度,但随机访问困难。许多高级编程语言都内置了链表的实现,可是C语言并无实现链表,因此Redis实现了本身的链表数据结构
链表在Redis中应用的很是广,列表(List)的底层实现就是链表。此外,Redis的发布与订阅、慢查询、监视器等功能也用到了链表
链表上的节点定义以下,adlist.h/listNode
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点值
void *value;
} listNode;
复制代码复制代码
链表的定义以下,adlist.h/list
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;
复制代码复制代码
每一个节点listNode
能够经过prev
和next
指针分布指向前一个节点和后一个节点组成双端链表,同时每一个链表还会有一个list
结构为链表提供表头指针head
、表尾指针tail
、以及链表长度计数器len
,还有三个用于实现多态链表的类型特定函数
dup
:用于复制链表节点所保存的值free
:用于释放链表节点所保存的值match
:用于对比链表节点所保存的值和另外一个输入值是否相等链表结构图
prev
和表尾节点的next
都指向NULL,对链表的访问以NULL结束len
属性,获取链表长度的复杂度为O(1)void*
指针保存节点值,能够保存不一样类型的值字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。字典中的每个键都是惟一的,能够经过键查找与之关联的值,并对其修改或删除
Redis的键值对存储就是用字典实现的,散列(Hash)的底层实现之一也是字典
咱们直接来看一下字典是如何定义和实现的吧
Redis的字典底层是使用哈希表实现的,一个哈希表里面能够有多个哈希表节点,每一个哈希表节点中保存了字典中的一个键值对
哈希表结构定义,dict.h/dictht
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,等于size-1
unsigned long sizemask;
// 哈希表已有节点的数量
unsigned long used;
} dictht;
复制代码复制代码
哈希表是由数组table
组成,table
中每一个元素都是指向dict.h/dictEntry
结构的指针,哈希表节点的定义以下
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,造成链表
struct dictEntry *next;
} dictEntry;
复制代码复制代码
其中key
是咱们的键;v
是键值,能够是一个指针,也能够是整数或浮点数;next
属性是指向下一个哈希表节点的指针,可让多个哈希值相同的键值对造成链表,解决键冲突问题
最后就是咱们的字典结构,dict.h/dict
typedef struct dict {
// 和类型相关的处理函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引,当rehash再也不进行时,值为-1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 迭代器数量
unsigned long iterators; /* number of iterators currently running */
} dict;
复制代码复制代码
type
属性和privdata
属性是针对不一样类型的键值对,用于建立多类型的字典,type
是指向dictType
结构的指针,privdata
则保存须要传给类型特定函数的可选参数,关于dictType
结构和类型特定函数能够看下面代码
typedef struct dictType {
// 计算哈希值的行数
uint64_t (*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;
复制代码复制代码
dict
的ht
属性是两个元素的数组,包含两个dictht
哈希表,通常字典只使用ht[0]
哈希表,ht[1]
哈希表会在对ht[0]
哈希表进行rehash
(重哈希)的时候使用,即当哈希表的键值对数量超过负载数量过多的时候,会将键值对迁移到ht[1]
上
rehashidx
也是跟rehash相关的,rehash的操做不是瞬间完成的,rehashidx
记录着rehash的进度,若是目前没有在进行rehash,它的值为-1
结合上面的几个结构,咱们来看一下字典的结构图(没有在进行rehash)
在这里,哈希算法和rehash(从新散列)的操做再也不详细说明,有机会之后单独介绍
当一个新的键值对要添加到字典中时,会根据键值对的键计算出哈希值和索引值,根据索引值放到对应的哈希表上,即若是索引值为0,则放到
ht[0]
哈希表上。当有两个或多个的键分配到了哈希表数组上的同一个索引时,就发生了键冲突的问题,哈希表使用链地址法来解决,即便用哈希表节点的next
指针,将同一个索引上的多个节点链接起来。当哈希表的键值对太多或太少,就须要对哈希表进行扩展和收缩,经过rehash
(从新散列)来执行
一个普通的单链表查询一个元素的时间复杂度为O(N),即使该单链表是有序的。使用跳跃表(SkipList)是来解决查找问题的,它是一种有序的数据结构,不属于平衡树结构,也不属于Hash结构,它经过在每一个节点维持多个指向其余节点的指针,而达到快速访问节点的目的
跳跃表是有序集合(Sorted Set)的底层实现之一,若是有序集合包含的元素比较多,或者元素的成员是比较长的字符串时,Redis会使用跳跃表作有序集合的底层实现
跳跃表其实能够把它理解为多层的链表,它有以下的性质
那么如何来理解跳跃表呢,咱们从最底层的包含全部元素的链表开始,给定以下的链表
而后咱们每隔一个元素,把它放到上一层的链表当中,这里我把它叫作上浮(注意,科学的办法是抛硬币的方式,来决定元素是否上浮到上一层链表,我这里先简单每隔一个元素上浮到上一层链表,便于理解),操做完成以后的结构以下
查找元素的方法是这样,从上层开始查找,大数向右找到头,小数向左找到头,例如我要查找17
,查询的顺序是:13 -> 46 -> 22 -> 17;若是是查找35
,则是 13 -> 46 -> 22 -> 46 -> 35;若是是54
,则是 13 -> 46 -> 54
上面是查找元素,若是是添加元素,是经过抛硬币的方式来决定该元素会出现到多少层,也就是说它会有 1/2的几率出现第二层、1/4 的几率出如今第三层......
跳跃表节点的删除和添加都是不可预测的,很难保证跳表的索引是始终均匀的,抛硬币的方式可让大致上是趋于均匀的
假设咱们已经有了上述例子的一个跳跃表了,如今往里面添加一个元素18
,经过抛硬币的方式来决定它会出现的层数,是正面就继续,反面就中止,假如我抛了2次硬币,第一次为正面,第二次为反面
跳跃表的删除很简单,只要先找到要删除的节点,而后顺藤摸瓜删除每一层相同的节点就行了
跳跃表维持结构平衡的成本是比较低的,彻底是依靠随机,相比二叉查找树,在屡次插入删除后,须要Rebalance来从新调整结构平衡
Redis的跳跃表实现是由redis.h/zskiplistNode
和redis.h/zskiplist
(3.2版本以后redis.h改成了server.h)两个结构定义,zskiplistNode
定义跳跃表的节点,zskiplist
保存跳跃表节点的相关信息
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// 成员对象 (robj *obj;)
sds ele;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
// 跨度其实是用来计算元素排名(rank)的,在查找某个节点的过程当中,将沿途访过的全部层的跨度累积起来,获得的结果就是目标节点在跳跃表中的排位
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
复制代码复制代码
zskiplistNode
结构
level
数组(层):每次建立一个新的跳表节点都会根据幂次定律计算出level数组的大小,也就是次层的高度,每一层带有两个属性-前进指针和跨度,前进指针用于访问表尾方向的其余指针;跨度用于记录当前节点与前进指针所指节点的距离(指向的为NULL,阔度为0)backward
(后退指针):指向当前节点的前一个节点score
(分值):用来排序,若是分值相同当作员变量在字典序大小排序obj
或ele
:成员对象是一个指针,指向一个字符串对象,里面保存着一个sds;在跳表中各个节点的成员对象必须惟一,分值能够相同zskiplist
结构
header
、tail
表头节点和表尾节点length
表中节点的数量level
表中层数最大的节点的层数假设咱们如今展现一个跳跃表,有四个节点,节点的高度分别是二、一、四、3
zskiplist
的头结点不是一个有效的节点,它有ZSKIPLIST_MAXLEVEL层(32层),每层的forward
指向该层跳跃表的第一个节点,若没有则为NULL,在Redis中,上面的跳跃表结构以下
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,能够保存类型为int16_t、int32_t、int64_t的整数值,而且保证集合中不会出现重复元素
整数集合是集合(Set)的底层实现之一,若是一个集合只包含整数值元素,且元素数量很少时,会使用整数集合做为底层实现
整数集合的定义为inset.h/inset
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
复制代码复制代码
contents
数组:整数集合的每一个元素在数组中按值的大小从小到大排序,且不包含重复项length
记录整数集合的元素数量,即contents数组长度encoding
决定contents数组的真正类型,如INTSET_ENC_INT1六、INTSET_ENC_INT3二、INTSET_ENC_INT64当想要添加一个新元素到整数集合中时,而且新元素的类型比整数集合现有的全部元素的类型都要长,整数集合须要先进行升级(upgrade),才能将新元素添加到整数集合里面。每次想整数集合中添加新元素都有可能会引发升级,每次升级都须要对底层数组已有的全部元素进行类型转换
升级添加新元素:
整数集合的升级策略能够提高整数集合的灵活性,并尽量的节约内存
另外,整数集合不支持降级,一旦升级,编码就会一直保持升级后的状态
压缩列表(ziplist)是为了节约内存而设计的,是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构,一个压缩列表能够包含多个节点,每一个节点能够保存一个字节数组或者一个整数值
压缩列表是列表(List)和散列(Hash)的底层实现之一,一个列表只包含少许列表项,而且每一个列表项是小整数值或比较短的字符串,会使用压缩列表做为底层实现(在3.2版本以后是使用quicklist
实现)
一个压缩列表能够包含多个节点(entry),每一个节点能够保存一个字节数组或者一个整数值
各部分组成说明以下
zlbytes
:记录整个压缩列表占用的内存字节数,在压缩列表内存重分配,或者计算zlend
的位置时使用zltail
:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,经过该偏移量,能够不用遍历整个压缩列表就能够肯定表尾节点的地址zllen
:记录压缩列表包含的节点数量,但该属性值小于UINT16_MAX(65535)时,该值就是压缩列表的节点数量,不然须要遍历整个压缩列表才能计算出真实的节点数量entryX
:压缩列表的节点zlend
:特殊值0xFF(十进制255),用于标记压缩列表的末端每一个压缩列表节点能够保存一个字节数字或者一个整数值,结构以下
previous_entry_ength
:记录压缩列表前一个字节的长度encoding
:节点的encoding保存的是节点的content的内容类型content
:content区域用于保存节点的内容,节点内容类型和长度由encoding决定上面介绍了Redis的主要底层数据结构,包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表。可是Redis并无直接使用这些数据结构来构建键值对数据库,而是基于这些数据结构建立了一个对象系统,也就是咱们所熟知的可API操做的Redis那些数据类型,如字符串(String)、列表(List)、散列(Hash)、集合(Set)、有序集合(Sorted Set)
根据对象的类型能够判断一个对象是否能够执行给定的命令,也可针对不一样的使用场景,对象设置有多种不一样的数据结构实现,从而优化对象在不一样场景下的使用效率
类型 | 编码 | BOJECT ENCODING 命令输出 | 对象 |
---|---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | "int" | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | "embstr" | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | "raw" | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | '"linkedlist' | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | "hashtable" | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | "intset" | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | "hashtable" | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | "ziplist" | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | "skiplist" | 使用跳跃表表实现的有序集合对象 |
参考:《Redis设计与实现》
原文连接:https://juejin.im/post/5d71d3bee51d453b5f1a04f1
声明:该文彻底转载,只为方便下次查找快速,若是侵权请联系本人