一、SDS(simple Dynamic String)
简单动态字符串
结构图:
三个属性:
len: 保存字符串的字节长度
free: 字节数组未使用的空间大小
buffer:字节数组,保存二进制数据。最后一个总是'\0',保持C字符串的惯例,复用部分C字符串的函数库。
新版本源码 (sds.h)
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[];
};
特点:
1、相比旧版本,每个字符串无论多长,都要占用8个字节的长度(free和len),新版本根据长度选择合适的sdshdr,节约空间。
2、C语言中字符串,没有记录字符串长度的属性,获取字符串长度,就要从头遍历到末尾,复杂度O(n);
3、SDS的len记录了字符串长度,复杂度O(1)。
4、C语言字符串拼接,可能造成缓冲区溢出(提前未分配足够的空间)。SDS会提前计算分配空间,不够的话,会自动扩展。
5、减少修改字符串时带来的内存重分配次数,C字符串底层关联着数组,修改字符串会带来内存的重新分配。
SDS的空间预分配:
1、len小于1M时,分配free的大小等于len,相当两倍的扩容
2、大于等于1M时,每次增加1M的未使用空间
SDS的惰性释放:
字符串缩减时,不会内存重分配,而是将释放的空间记录加在free上,为下次的字符串追加做准备。
二、链表
源码(adlist.h)
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;
list持有更方便
结构图:
特点:
1、获取头节点和尾节点的复杂度是O(1)
2、获取链表的长度也是O(1)
3、多态,使用指针保存值,支持多种类型的值
广泛用在列表键,发布订阅,监视器,慢查询等。
三、字典
字典又称符号表,关联数组,映射(map),存储键值对的一种数据结构抽象。
数据库底层的存储结构,hash键的底层实现之一(键值对比较多或者元素比较长)。
Redis字典使用哈希表实现,一个哈希表包含多个哈希表节点,每个节点存储字典的一个键值对。
结构图:
源码(dict.h)
struct dict {
dictType *type; //键值对类型,为创建多态字典设计
dictEntry **ht_table[2]; //两张hash表
unsigned long ht_used[2]; //两张哈希表中,分别使用了多少
long rehashidx;
int16_t pauserehash;
signed char ht_size_exp[2];
};
ht_table[2] //两张哈希表,一般情况只使用ht_table[0],ht_table[1]在 rehash时使用,rehashidx是-1,则代表没有进行rehash。
typedef struct dictEntry {
void *key; //键
union {
void *val; //值可以是指针
uint64_t u64; //值是uint64_t
int64_t s64; //值是int64_t
double d; //值是double
} v;
struct dictEntry *next; //下一个节点,用次解决hash冲突,采用头插法
void *metadata[];
} dictEntry;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); //计算hash 函数值
void *(*keyDup)(dict *d, const void *key); //复制键
void *(*valDup)(dict *d, const void *obj); //复制值
int (*keyCompare)(dict *d, const void *key1, const void *key2); //对比键
void (*keyDestructor)(dict *d, void *key); //销毁键
void (*valDestructor)(dict *d, void *obj); //销毁值
int (*expandAllowed)(size_t moreMem, double usedRatio);
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
rehash过程:
1、如果是扩容则扩到第一个大于等于 ht_table[0]使用空间的2倍且是 2的n次幂。
2、如果是收缩,则是 第一个大于等于 ht_table[0]使用空间且是 2的n次幂。
3、将ht_table[0]上的建rehash到ht_table[1]上,全部rehash到ht_table[1]后,则释放ht_table[0],将ht_table[1]设为ht_table[0],在ht_table[1]上创建空白的哈希表,为下一次的rehash做准备。
哈希表扩容和收缩的条件
1、服务器没有执行bgsave 或bgrewriteaof,且负载因子大于等于1
2、服务器正在执行bgsave 或bgrewriteaof,且负载因子大于等于5
3、负载因子小于0.1时,执行哈希表的收缩
负载因子 = ht_used[0] / ht_table[0]
渐进式rehash步骤:
1、为 ht_table[1]分配空间,让字典同事持有 ht_table[0]和 ht_table[1]
2、字典维持一个索引计数器rehashidx,值设为0,标识rehash正式开始
3、在rehash期间,字典执行增删改查时,程序除了执行操作,还要将ht_table[0]在rehashidx上的所有键值对迁移到ht_table[1]上,完成后rehashidx加1
4、随着字典的不断操作,所有的ht_table[0]上的键值对都会被rehash到ht_table[1]上,完成迁移,rehashidx 设为-1
注意:
渐进式rehash过程,如果查询字典,则会分别查询ht_table[0]和 ht_table[1],新增时,则会增加到ht_table[1]上,保证ht_table[0]只减不增。
四、跳跃表
跳跃表是一个有序的数据结构,通过在每个节点维持多个指向其他节点的指针,从而达到快速访问的目的。
复杂度平均O(logN)最坏O(N)
有序集合键的底层实现之一(元素数量比较多或者键比较大),另一个是集群节点中用作内部数据结构
结构图:
源码( server.h):
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;
层数是1至32的随机数
节点按照分值从小到大排序,分值相同按照成员排序。
五、整数集合
集合键的底层实现之一(集合只包含整数元素,并且元素数量不多)
源码(intset.h):
typedef struct intset {
uint32_t encoding; //编码方式
uint32_t length; //元素数量
int8_t contents[]; //保存元素的数组,类型取决于encoding属性,从小到大排序
} intset;节点
结构图:
contents[]升级,当数组中都是int16_t,突然加入一个int64_t,则会将所有的元素都升级为int64_t。
只支持升级,不支持降级。
升级的好处:提升灵活性,节约内存。
升级过程:
1、扩展整数集合底层数组的空间大小
2、将原元素转换成新元素类型,从后往前迁移,放在合适的位置
3、将新元素添加到合适位置
六、压缩列表
列表键(列表项比较少或者列表项是小整数或者短字符串)和哈希建的底层实现之一。
结构图:
压缩列表组成部分
zlbytes: 占用的字节数
zltail:记录压缩列表的尾节点距离到压缩列表的起始地址有多少字节
z1len: 节点数量,大于65535时,只能遍历节点才能知道数量
entryn: 压缩列表节点,字节数组或者一个整数值
zlend:特殊0xFF,标记压缩列表的末端
每个压缩列表节点:
previous_entry_length : 前一个节点的字节长度,使用一个字节或者5个字节存储,用它可以回溯到压缩列表的头部
encoding :记录content属性的类型和长度
content :节点的值
连锁更新:
增加一个大于254长度的节点,为了让previous_entry_length满足压缩列表对节点的要求,从头到尾的更新空间大小,执行空间重分配。
删除节点也可能引发连锁更新。
恰好有多个,连续的,长度介于250到253之间的节点才会引起连锁更新,连锁更新的节点不多,不会影响性能。
七、quicklist
quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据。
typedef struct quicklist {
quicklistNode *head; //头节点
quicklistNode *tail; //尾节点
unsigned long count; //ziplist节点计数器
unsigned long len; //quicklist的quicklistNode节点计数器
signed int fill : QL_FILL_BITS; //保存ziplist的大小,配置文件设定,占16bit
unsigned int compress : QL_COMP_BITS; //保存压缩程度值,配置文件设定,占16bits,0表示不压缩
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; //前驱节点
struct quicklistNode *next; //后继节点
unsigned char *entry; //不设置压缩数据参数recompress时指向一个ziplist结构
//设置压缩数据参数recompress指向quicklistLZF结构
size_t sz; //压缩列表ziplist的总长度
unsigned int count : 16; //ziplist中包的节点数,占16 bits长度
unsigned int encoding : 2; //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
unsigned int container : 2; //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
unsigned int recompress : 1; //如果recompress为1,则等待被再次压缩
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
typedef struct quicklistLZF {
size_t sz; //表示被LZF算法压缩后的ziplist的大小
char compressed[]; //保存压缩后的ziplist的数组,柔性数组
} quicklistLZF;
//管理quicklist中quicklistNode节点中ziplist信息的结构
typedef struct quicklistEntry {
const quicklist *quicklist; //指向所属的quicklist的指针
quicklistNode *node; //指向所属的quicklistNode节点的指针
unsigned char *zi; //指向当前ziplist结构的指针
unsigned char *value; //指向当前ziplist结构的字符串vlaue成员
long long longval; //指向当前ziplist结构的整数value成员
unsigned int sz; //保存当前ziplist结构的字节数大小
int offset; //保存相对ziplist的偏移量
} quicklistEntry;
结构图: