String:
底层数据结构
SDS (simple dynamic string,简单动态字符串)
为什么不使用c语言的char*实现string?:
1.获取长度需要o(n)遍历至’/0’
2.字符串内不能含’/0’ ->不能存二进制文件
3.字符串拼接是直接在dest后面赋值src的内容,dest原内存不足可能会出现缓冲区溢出,导致程序退出
c语言的拼接函数:
char *strcat(char *dest, const char* src)
SDS数据结构:
struct __attribute__ ((__packed__)) hisdshdr8 {
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[];
};
Len: 通过成员变量获取字符串长度 O(1),通过len与计数来判断是否完成遍历
Alloc:在修改字符串时,通过alloc-len判断当前空间是否满足需求,否则进行拓容(小于1M翻倍,大于则加1M),不仅避免缓冲区溢出的问题,还减少了内存的分配次数
flags:指定字符串类型,通过按长度需求,减少结构占用内存
Buf:底层字符数组,可以存二进制文件
编译器:对采用**__attribute__属性的结构体,能够跳去字节对齐**,按结构体实际占用空间分配内存,节省内存空间
List:
底层数据结构:
linkedList
、zipList
和quickList
linkedList:
节点结构:
typedf struct listNode{
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
头结点结构:
typedf struct list{
listNode *head;
listNode *tail;
//节点拷贝函数
void *(*dup)(void *ptr);
//释放节点函数
void *(*free)(void *ptr);
//判断两个节点是否相等的函数
int (*match)(void *ptr,void *key);
//链表长度
unsigned long len;
}
缺陷:
节点之间的内存大部分都不是连续的,不能利用好CPU以页载入的缓存。
就算list只有一个节点,也需要进行链表头节点的结构体分配,链表在数量不多时造成内存浪费
zipList:
由连续内存块组成的顺序数据结构,类似数组
优点:连续分配,内存使用效率高(指减少了内存碎片以及节点分配带来的内存消耗)
数据结构:
typedf struct ziplist<T>{
//压缩列表占用字节数
int32 zlbytes;
//尾节点离起始位置的偏移量,用于快速定位尾节点
int32 zltail_offset;
//元素个数
int16 zllength;
//元素内容
T[] entries;
//结束位 0xFF
int8 zlend;
}ziplist
对队头和队尾的查询为O(1),其他为O(n), 支持双向遍历,但是不适合保存太多元素
节点:
typede struct entry{
//前一个entry的长度
int<var> prelen;
//元素类型编码
int<var> encoding;
//元素内容
optional byte[] content;
}entry
更新节点可能会出现 连锁更新
即:由于ziplist的内存大小是连续分配的,当节点原分配的空间大小不足以满足修改后的节点,需要给该节点重新分配内存,后面节点的空间也都要重新分配
- linkedList与zipList 对比:
节点数量少时,使用zipList的存储效率更高
节点数量大时,linkedList的维护效率更高
quickList:
是zipList和linkedList的组合,通过linkedList分段,zipList存储数据,可选配LZF压缩算法进行数据压缩
节点结构:
typedf struct quicklistNode{
quicklistNode* prev;
quicklistNode* next;
//压缩列表
ziplist* zl;
//ziplist大小
int32 size;
//ziplist 中元素数量
int16 count;
//编码形式 存储 ziplist 还是进行 LZF 压缩储存的zipList
int2 encoding;
...
}quickListNode
头结点结构:
typedf struct quicklist{
//指向头结点
quicklistNode* head;
//指向尾节点
quicklistNode* tail;
//元素总数
long count;
//quicklistNode节点的个数
int nodes;
//压缩算法深度
int compressDepth;
...
}quickList
redis.conf中定义默认ziplist长度为8k字节,也可以设置数量来限制单个ziplist最多n个节点
HashTable:
底层数据结构:
// 双哈希表,用于拓容时渐进式rehash
typedef struct dict {
// ...
// 两个哈希表,交替使用,用于 rehash 操作
dictht ht[2];
// 哈希表rehash进度条
// -1 表示没有进行 rehash
long rehashidx;
//...
} dict;
typedef struct dictht {
// 数组的首地址,哈希表是通过数组实现的
// 而数组中的每个元素都是一个 dictEntry *
// 所以这里 table 的类型是 dictEntry **
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
unsigned long sizemask;
// 该哈希表已有的节点数量
unsigned long used;
} dictht;
typedef struct dictEntry {
// 指向键值对中的键
void *key;
// 指向键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
- 通过union结构,实现动态根据val的值来选择存储数据类型,当为结构体时则使用指针,浮点数用double… 来减少多余的空间浪费,节省内存
- 哈希表通过(哈希值%哈希表大小(哈希桶数量))获得对应元素的存储位置(底层数组)
哈希冲突
指两个及以上的key被分配到了同一个桶上
Redis通过链式哈希(拉链法)解决哈希冲突。
局限性:随着链表长度增加,查询耗时也会增加(极端下O(N))
解决办法:rehash,对哈希表进行调整,并重新计算各个key的hash并分配位置
Rehash:
步骤:
- 给哈希表2分配大于哈希表1 两倍的2次幂空间
- 将哈希表1的数据复制到哈希表2
- 释放哈希表1的空间,将哈希表2设置为哈希表1
- 新建个哈希表作为哈希表2
在数据迁移期间,如果数据量过大,会阻塞redis-server服务。
Redis使用渐进式rehash解决
渐进Rehash:
步骤:
- 给哈希表2分配大于哈希表1两倍的2次幂空间,初始化rehashidx为0
- 每次对哈希表进行操作(crud)时,redis额外将哈希表1对应rehashidx的桶复制到哈希表2,在哈希表1删除对应桶,rehashidx++;
- 反复操作直至rehashidx>哈希表1桶数量
注:查询/删除/更新时,在哈希表1,2中查,新增时只在哈希表2新增 保证哈希表1只删不增
Rehash条件:
- ht[0] 的大小为 0;
- ht[0].used > ht[0].size,即哈希表内的元素数量已经超过哈希表的大小,并且可以扩容;
- ht[0].used 大于等于 ht[0].size 的 5 倍
Set:
底层数据结构:**
数据量不大,且value类型为整型:inset
其他情况:dict(hash)
inset:
typedf struct inset{
// 编码方式有三种
// 默认 INSET_ENC_INT16
uint32_t encoding;
// 集合元素个数
uint32_t length;
// 实际存储元素的数组
int8_t contents[];
}inset;
# 16位,2个字节,表示范围 -2^16 ~ 2^16
# define INTSET_ENC_INT16 (sizeof(int16_t))
# 32位,4个字节,表示范围 -2^32 ~ 2^32
# define INTSET_ENC_INT32 (sizeof(int32_t))
# 64位,8个字节,表示范围 -2^64 ~ 2^64
#define INTSET_ENC_INT64 (sizeof(int64_t))
升级:
指出现元素大小超过当前编码方式时,改变编码方式及执行相关操作的过程
升级过程:
- 计算当前已有元素内存占用大小 len*encoding
- 选择支持当前最大元素大小的新编码方式
- 计算转换成新编码所需新增的内存大小,尾插
- 从插入新数据的起始idx开始,将之前数据按新编码方式往前进行插入
优点:能够节约内存,只在必要时扩容
缺点:不支持降级
Zset:
底层数据结构:
zipList、skipList(dict(哈希表的字典)、zskipList)
-
当元素大小<128&&member长度<64B时,使用zipList
-
其他情况skipList一把梭
skipList:
//链表节点
typedef struct zskiplistNode {
robj *obj;//元素指针
double score;//排序分数
struct zskiplistNode *backward;//指向前一元素指针
struct zskiplistLevel {//层级
struct zskiplistNode *forward;//后面元素指针
unsigned int span;
} level[];
} zskiplistNode;
//跳表头信息
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;//跳表层级
} zskiplist;