1、SDS(简单动态字符串)
空间分配: 初始(小于1M时扩容)空间:字符串长度2倍字节,大于1M时每次扩容增加1M空间,多余空间作 为缓冲区
惰性空间释放: 用于优化SDS的字符串缩短操作,由redis自己实现(调用内部API)空间释放
struct sdshdr{
// 记录buf数组中已使用字节的数量
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
2、链表
2.1 用于列表键、发布、订阅、慢查询、监视器等
2.2 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以链表是双 向链表
2.3 每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针一级链表长度等信息
2.4 因为链表表头、表尾节点的后置节点都指向null,所以redis的链表实现为无环链表
typedef struct list{
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值赋值函数 用于赋值链表节点所保存的值
void *(dup)(void *ptr);
// 节点值释放函数 用于释放链表节点所保存的值
void (*free)(void *ptr);
// 节点对比函数 match函数用于对比链表节点所保存的值和另外一个输入值是否相等
int (*match)(void *ptr,void *key);
} list;
typedef struct listNode{
// 前置节点
struct listNode*prev;
// 后置节点
struct listNode*next;
// 节点的值
void *value;
}
链表特点
双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(1)
无环:表头节点的prev和表尾节点的next指针都指向null,对链表的访问以null为终点
多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点设置特定类型,所以链表可以用于保存各种不同类型的值
3、字典
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。
3.1 使用哈希表作为底层实现
存在问题及解决方案:
3.1.1 键冲突:redis哈希表使用链地址(hashmap也是,不同的是java8map使用尾插法,这里使用头插法)发来解决键冲突
3.1.2 rehash(重新散列):用来对哈希表进行扩容和收缩 3.1.3 渐进式哈希:扩展或搜索哈希表时需要将ht[1]里面的所有键值对rehash到ht[0],当键值对数量较大时可能会导致服务器停止服务 步骤: 1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表 2)再字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始 3)再rehash期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了rehash了指定操作外,还会将 ht[0]中哈希表再rehashidx索引上的所有键值对rehash到ht[1],并执行rehashidx++ 4)随着字典操作执行,ht[0]的所有键值对会被复制到ht[1],这时将rehashidx值设为-1,表示rehash已完成
扩容负载因子公式:负载因子 = 哈希表已保存节点数量 / 哈希表大小
收缩: 负载因子小于0.1时,对哈希表进行收缩
扩容: 当以下条件中任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有执行BGSAVE或BGREWRITEAOF命令,并且哈希表负载因子大于等于1。
- 服务器正在执行BGSAVE或BGREWRITEAOF命令,并且哈希表负载因子大于等于5。
区分这两种情况的目的在于,因为执行BGSAVE与BGREWRITEAOF过程中,Redis都需要创建子进程,而大多数操作系统都采用写时复制技术来优化子进程使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入,最大限度的节约空间。
// 哈希表
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码 用于计算索引值
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
// 哈希表节点
typedef struct dictEntry{
// 键
void *key;
// 值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
// 指向下一个哈希表节点,形成链表
structt dictEntry *next;
}
// 字典
typedev struct dict{
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表 一般情况下只使用ht[0]哈希表,扩展或搜索哈希表时需要将ht[1]里面的所有键值对rehash到ht[0]
dictht ht[2];
// rehash索引 当rehash不在进行时,值为-1
int trehashidx;
} dict;
// 字典类型
typedev 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,void *key,const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata,void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata,void *obj);
} dictType;
4、跳跃表
一种有序的数据结构,通过每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的
跳表是作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,redis就会使用跳表来作为有序集合键实现
// 跳跃表数据结构
typedef struct zskiplist{
// 表头、表尾节点
structz skiplistNode *header,*tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
// 跳表节点数据结构
typedef struct zskiplistNode{
// 层
// level数组可以包含多个元素 每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层数越多,访问其他节点的速度越快
struct zskiplistLevel{
// 前进指针
// 用于从表头向表尾方向访问节点
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
// 后退指针
// 用于从表尾向表头方向访问节点(访问前一个节点)
struct zskiplistNode *backward;
// 分值
// 表级别 表示节点按从小到大排序
double *obj;
// 成员对象
// 节点成员对象 有序,较小的排在表头
robj *obj;
} zskiplistNode;
5、整数集合
用于保存整数值的集合抽象数据结构,可以保存int16_t、int32_t、int64_t的整数值,并且集合中元素不重复
redis集合底层实现之一,当集合只包含数值元素并且数量不多时,redis使用整数集合作为底层实现
typedef struct intset{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
6、压缩列表
redis列表和哈希底层实现之一,当列表键只包含少量列表元素,并且每个元素的长度比较短,那么redis就会使用压缩列表来做底层实现
压缩列表是未来节约内存开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或一个整数值
6.1 压缩列表节点构成
每个节点都有3部分组成
previous_entry_length: (前一个节点的长度)以字节为单位,如果前一个节点的长度(content)小于254字节,那么使用1字节保存,如果大于等于254字节,使用5字节保存,器中第一个字节会设置为0xFE,之后的4个节点保存前一节点的长度
缺点也很明显:删除节点的时候数据移动,也有可能节点的previous_entry_length由1字节变为5字节从而需要改变下一字节的previous_entry_length
encoding: 节点content保存的数据的编码类型(编码格式不同占用内存长度也不同)及长度
content: 保存节点的值
// ziplist创建
unsigned char *ziplistNew(void) {
// 分配压缩列表结构所需要的字节数
// ZIPLIST_HEADER_SIZE=zlbytes(4)+zltail(4)+zllen(2)
unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
// 分配内存
unsigned char *zl = zmalloc(bytes);
// 初始化 ZIPLIST_BYTES 字段
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
// 初始化 ZIPLIST_TAIL_OFFSET
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
// 初始化 ZIPLIST_LENGTH 字段
ZIPLIST_LENGTH(zl) = 0;
// 为压缩列表最后一个字节赋值 255
zl[bytes-1] = ZIP_END;
// 返回起始地址
return zl;
}
// ===================== 插入元素开始 ========================
// 在zl的位置p处插入一个元素,元素的内容为s,s的长度为slen
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /*任意一个非0值*/
zlentry tail;
/* 首先找到待插入位置的前一个节点的长度prevlen*/
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
}
/* 首先判断节点是否可以被编码,并选择合适的编码方式 */
if (zipTryEncoding(s,slen,&value,&encoding)) {
reqlen = zipIntSize(encoding);
} else {
reqlen = slen;//若不能被编码,直接以字符串形式存储
}
/* 还需要存储前一个节点的长度和当前节点的长度 */
reqlen += zipPrevEncodeLength(NULL,prevlen);
reqlen += zipEncodeLength(NULL,encoding,slen);
/* 当插入位置不为tail时,需要保证后一个节点的prevlen字段足够存储当前节点的长度 */
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
/* 保存偏移量 */
offset = p-zl;
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset;
/* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) { //当插入位置不为tail时
/* 将p之后的所有内容向后移动到p+reqlen */
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
/*将当前节点的长度经编码后存储到下一个节点的prevlen中*/
zipPrevEncodeLength(p+reqlen,reqlen);
/* 更新zltail的值 */
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
tail = zipEntry(p+reqlen);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else { //插入到列表尾部
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
}
if (nextdiff != 0) {
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
}
/* 将新元素写到位置p */
p += zipPrevEncodeLength(p,prevlen);
p += zipEncodeLength(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}
// ===================== 插入元素结束 ========================
// 查找元素
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
int skipcnt = 0;
unsigned char vencoding = 0;
long long vll = 0;
while (p[0] != ZIP_END) { //从头遍历到最后一个节点
unsigned int prevlensize, encoding, lensize, len;
unsigned char *q;
ZIP_DECODE_PREVLENSIZE(p, prevlensize);
ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
q = p + prevlensize + lensize;
if (skipcnt == 0) {
if (ZIP_IS_STR(encoding)) { //如果没有编码,直接以字符串形式比较
if (len == vlen && memcmp(q, vstr, vlen) == 0) {
return p;
}
} else {
if (vencoding == 0) { //否则对vstr进行编码
if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
vencoding = UCHAR_MAX;
}
assert(vencoding);
}
if (vencoding != UCHAR_MAX) {
long long ll = zipLoadInteger(q, encoding);
if (ll == vll) { //然后再与节点数据比较
return p;
}
}
}
skipcnt = skip;
} else {
skipcnt--;
}
p = q + len;
}
return NULL;
}