1 SDS
struct sdshdr{
// 记录buf数组中已经使用的字节数量
// 等于SDS所保存的字符串的长度
int len;
// 记录buf数组中未使用的字节数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
SDS
为什么性能高
-
常数复杂度获取字符串长度【O(1)】,C语言字符串需要遍历一次【O(N)】
-
不用检查是否会溢出,
SDS
函数接口会帮我们检查是否会char数组溢出,如果溢出会申请一块新的更大的内存地址来保存字符串 -
但是每次都重新分配空间,然后进行数组数据拷贝到新数组中,很耗费性能。所以像
ArrayList
一样,预先分配更多空间,SDS
长度小于1MB
的时候倍增,大于1MB
的时候1MB
的步进方式增加容量 -
惰性释放空间,缩短字符串时,并不是立即重新分配字符串空间,直接修改’\0’的位置或者执行类似
System.arraycopy()
,而是使用free属性记录剩余空间大小
二进制安全
之前C语言字符串都是以\0
来判断一个字符串是否结束,不管`\0之后还有没有可读的字符。
然而char buf[]
是字节数组,而不是字符数组,判断一个字符串结束是以len
属性来判断,而不是\0
2 链表
typedef struct listNode{
// 前后节点指针
struct listNode *prev;
struct listNode *next;
// 节点值
void *value;
}listNode
typedef struct list{
listNode *head;
listNode *tail;
// 链表节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *prt);
// 节点值释放函数
void *(*free)(void *prt);
// 节点值对比函数
int *(*match)(void *prt, void *key);
}
list
结构体用来持有链表的双端链表
dup()
用于复制节点保存的值free()
用于释放节点保存的值match()
对比链表节点保存的值是否与另一个输入值相等
特性
-
双端
-
无环,头结点的
pre
,尾结点的next
都指向null
-
查看链表元素数量
O(1)
时间复杂度 -
多态,链表节点使用
void*
,可以用于保存各种不同类型的值
3 哈希表
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小,也就是table数组的长度
unsigned long size;
// 哈希表掩码值,用于计算索引值,总是等于size-1
unsigned long sizemask;
// 哈希表已有节点数量
unsigned long used;
}
typedef struct dictEntry{
// 键
void *key;
// 值【值可以是一个指针,也可以是uint64_t或者int64_t类型整数】
union{
void *val;
uint64_t u64;
int64_t s64;
}
// 指向下一个节点,形成链表,发生哈希冲突的时候使用
struct dictEntry *next;
}
dictEntry **table;
是一个指针数组,所以使用了两个*。size也就是table数组的长度。
K-V键值对,K唯一。
应用—字典的实现
typedef struct dict {
// 类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
dictType *type;
// 私有数据,privdata 属性则保存了需要传给类型特定函数的可选参数。
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
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;
ht
属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht
哈希表, 一般字典只使用 ht[0]
哈希表, ht[1]
哈希表只会在对 ht[0]
哈希表进行 rehash 时使用。ht
相当于是一个滚动数组,当哈希表扩容或者缩小的时候都会进行rehash操作,移动顺序就 0 -> 1,1 -> 0。 dictType *type;
类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数。
字典哈希算法
如果我们要将一个键值对 k0
和 v0
添加到字典里面, 那么程序会先使用语句:
hash = dict->type->hashFunction(k0);
dictType *type;
类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数。
计算键 k0
的哈希值。
假设计算得出的哈希值为 8
, 那么程序会继续使用语句:
index = hash & dict->ht[0].sizemask = 8 & 3 = 0;
计算出键 k0
的索引值 0
, 这表示包含键值对 k0
和 v0
的节点应该被放置到哈希表数组的索引 0
位置上
字典rehash
为了哈希表的负载因子维持在一个合理的范围内,哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩。
-
为字典的
ht[1]
哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及ht[0]
当前包含的键值对数量 (也即是ht[0].used
属性的值):-
如果执行的是 扩展操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used * 2
的2^n。 -
如果执行的是 收缩操作, 那么
ht[1]
的大小为第一个大于等于ht[0].used
的2^n。
-
-
将保存在
ht[0]
中的所有键值对 rehash 到ht[1]
上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到ht[1]
哈希表的指定位置上。 -
当
ht[0]
包含的所有键值对都迁移到了ht[1]
之后, 释放ht[0]
, 将ht[1]
设置为ht[0]
, 并在ht[1]
新创建一个空白哈希表, 为下一次 rehash 做准备。
渐进式 rehash
如果哈希表里中数据量巨大, 那么要一次性将这些键值对全部 rehash 到 ht[1]
的话,可能会导致服务器在一段时间内停止服务。所以要分多次、渐进式地完成rehash。
- 为
ht[1]
分配空间, 让字典同时持有ht[0]
和ht[1]
两个哈希表。 - 在字典中维持一个索引计数器变量
rehashidx
, 并将它的值设置为0
, 表示 rehash 工作正式开始。 - 在 rehash 进行期间, 每次对字典CRUD操作时, 还会顺带将
ht[0]
哈希表在rehashidx
索引上的所有键值对 rehash 到ht[1]
, 当 0号槽rehash 工作完成之后, 将rehashidx
属性的值增一。 - 最终
ht[0]
的所有键值对都会被 rehash 至ht[1]
, 这时**将rehashidx
属性的值设为-1
,表示 rehash 操作已完成 ** 。
4 跳跃表
持有跳表的数据结构
header
:指向跳跃表的表头节点。tail
:指向跳跃表的表尾节点。level
:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。length
:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。
跳表节点
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
score
属性是一个double类型,跳跃表中的所有节点都按分值从小到大来排序。obj
属性代表成员对象,是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS
值。backward
属性是后退指针,用于从表尾向表头方向访问节点:每次只能后退至前一个节点。level
数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,可以通过这些层来加快访问其他节点的速度level[i].forward
属性,用于从表头向表尾方向访问节点。level[i].span
属性,用于记录两个节点之间的距离:跨度用来计算排位(rank)的:在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位。
5 整数集合
typedef struct intset {
// 编码方式决定存储的是int16_t 、 int32_t 或者 int64_t
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
升级(不支持降级)
假设现在有一个整数集合, 集合中包含三个 int16_t
类型的元素
每个元素都占用 16
位空间, 所以整数集合底层数组的大小为 3 * 16 = 48
位
int32_t
整数值 65535
添加到整数集合, int32_t
比整数集合当前所有元素的类型都要长,需要先对整数集合进行升级。
每个 int32_t
整数值需要占用 32
位空间,空间重分配之后, 底层数组的大小将是 32 * 4 = 128
位
最终变成
6 压缩列表
- 列表
zlbytes
属性表示压缩列表的总字节长。 - 列表
zltail
属性, 如果我们有一个指向压缩列表起始地址的指针p
, 那么只要用指针p
加zltail
属性 , 就可以计算出表尾节点的地址。 - 列表
zllen
属性表示压缩列表包含节点。
每个压缩列表节点都由previous_entry_length
、 encoding
、 content
三个部分组成
如果我们有一个指向当前节点起始地址的指针 c
, 用指针 c
减去当前节点 previous_entry_length
属性值, 就可以得出一个指向前一个节点起始地址的指针。
previous_entry_length
previous_entry_length
属性的长度可以是 1
字节或者 5
字节:
- 如果前一节点的长度小于
254
字节, 那么previous_entry_length
属性的长度为1
字节: 前一节点的长度就保存在这一个字节里面。 - 如果前一节点的长度大于等于
254
字节, 那么previous_entry_length
属性的长度为5
字节: 其中属性的第一字节会被设置为0xFE
(十进制值254
), 而之后的四个字节则用于保存前一节点的长度。
压缩列表的从表尾向表头遍历操作就是使用这一原理实现的: 只要我们拥有了一个指向某个节点起始地址的指针, 那么通过这个指针以及这个节点的 previous_entry_length
属性, 程序就可以一直向前一个节点回溯, 最终到达压缩列表的表头节点。
encoding
记录了节点的 content
属性所保存数据的类型以及长度
10
开头五个字节的ByteArray
00
开头一个字节的ByteArray
01
开头两个字节的ByteArray
content
负责保存节点的值, 节点可以保存一个ByteArray
或者一个整数值, 值的类型和长度由节点的 encoding
属性决定。
连锁更新
后一个节点NodeNext
的previous_entry_length
属性记录着前一个节点NodePre
的长度,
当NodePre
增长导致NodeNext
的属性值也要增加,如果NodeNext
的pre...Len
增加过程中超过254,自己也要发生相应变化
7 五种对象
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// 引用计数
int refcount;
// 对象最后一次被命令程序访问的时间
unsigned lru:22;
} robj;
type
类型一览:
对象 | 对象 type 属性的值 | TYPE 命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING | "string" |
列表对象 | REDIS_LIST | "list" |
哈希对象 | REDIS_HASH | "hash" |
集合对象 | REDIS_SET | "set" |
有序集合对象 | REDIS_ZSET | "zset" |
编码和底层实现
ptr
指针指向对象的底层实现数据结构,encoding
属性记录了对象所使用的编码:
encoding 编码常量 | 编码所对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long 类型的整数 |
REDIS_ENCODING_EMBSTR | embstr 编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
每种类型的对象都至少使用了两种不同的编码
对象 | String
: Int + embStr + raw
| 编码。
对象 | list
: 压缩列表 + 链表 | 编码。
对象 | hash
:压缩列表 + 字典 | 编码。
对象 | set
:整数集合 + 字典 | 编码。
对象 | zset
:压缩列表 + 跳表 | 编码。
8 字符串对象
**String
: Int + embStr + raw
**。
一个字符串对象保存的是整数值, 并且这个整数值可以用 long
类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr
属性里面(将 void*
转换成 long
), 并将字符串对象的编码设置为 int
。
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39
字节 , 那么字符串对象将使用一个简单动态字符串(SDS
)来保存这个字符串值, 并将对象的编码设置为 raw
。字符串值的长度小于等于 39
字节, 那么字符串对象将使用 embstr
编码的方式来保存这个字符串值。
-
embst
所需的内存分配次数一次,raw
两次,分别为redisObject
和sdshdr
分配两次内存 。 -
释放
embstr
只需要调用一次内存释放函数, 而释放raw
需要调用两次内存释放函数 。 -
因为
embstr
的所有数据都保存在一块连续的内存里面, 比起raw
能够更好地利用缓存 带来的优势。
int
embstr
满足条件的情况下会转换成 raw
9 列表对象
**list
: 压缩列表 + 链表 **。
redis> RPUSH numbers 1 "three" 5
(integer) 3
压缩列表形式:
链表形式:
StringObject
对象的真正格式 ↓
编码格式转化:
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist
编码:
- 列表对象保存的所有字符串元素的长度都小于
64
字节; - 列表对象保存的元素数量小于
512
个;
不能满足这两个条件的列表对象需要使用 linkedlist
编码。
10 哈希对象
hash
:压缩列表 + 字典。
压缩列表
- 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
字典
- 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。
编码转换
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist
编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于
64
字节; - 哈希对象保存的键值对数量小于
512
个;
不能满足这两个条件的哈希对象需要使用 hashtable
编码。
11 集合对象
set
:整数集合 + 字典。
redis> SADD numbers 1 3 5
(integer) 3
整数集合
字典
hashtable
作为底层实现时字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合中的元素, 而**字典的值则全部被设置为 NULL
**。
编码的转换
当集合对象可以同时满足以下两个条件时, 对象使用 intset
编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过
512
个;
不能满足这两个条件的集合对象需要使用 hashtable
编码。
12 有序集合对象
zset
:压缩列表 + 跳表。
压缩列表
每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员, 而第二个元素则保存元素的分值。
分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
跳表
zsl
跳跃表按分值从小到大保存了所有集合元素。
字典为有序集合创建了一个从成员到分值的映射。可以O(1)
时间复杂度内获得成员的分值。不用O(logn)
遍历跳表了。
typedef struct zset {
//zsl 跳跃表按分值从小到大保存了所有集合元素
zskiplist *zsl;
// 字典为有序集合创建了一个从成员到分值的映射
dict *dict;
} zset;
编码的转换
当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist
编码:
- 有序集合保存的元素数量小于
128
个; - 有序集合保存的所有元素成员的长度都小于
64
字节;
13 内存回收
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// 引用计数
int refcount;
// 对象最后一次被命令程序访问的时间
unsigned lru:22;
} robj;
应该就像是Netty中预先申请一大块内存自己管理一样,Redis肯定也会因为压榨性能而自己预先申请一大块内存。
既然申请了就得自己好好管理,使用引用计数器 refcount
,
- 在创建一个新对象时, 引用计数的值会被初始化为
1
; - 当对象被一个客户端使用时, 它的引用计数值会被增一;
- 当对象不再被一个程序使用时, 它的引用计数值会被减一;
- 当对象的引用计数值变为
0
时, 对象所占用的内存会被释放。
// 创建一个字符串对象 s ,对象的引用计数为 1
robj *s = createStringObject(...)
// 对象 s 执行各种操作 ...
// 将对象 s 的引用计数减一,使得对象的引用计数变为 0
// 导致对象 s 被释放
decrRefCount(s)
14 对象共享
A 创建了一个包含整数值 100
的字符串对象, B 也要创建一个同样保存了整数值 100
可以让键 A 和键 B 共享同一个字符串对象
多个键共享同一个值对象需要执行以下两个步骤:
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
15 对象的空转时长
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// 引用计数
int refcount;
// *******对象最后一次被命令程序访问的时间*******
unsigned lru:22;
} robj;
如果服务器打开了 maxmemory
选项, 并且服务器用于回收内存的算法为 volatile-lru
或者 allkeys-lru
, 那么当服务器占用的内存数超过了 maxmemory
选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。