Redis是一种基于键值对(key-value)的 NoSQL 数据库,它支持存储多种类型数据结构,因为Redis会把所有的数据都存放在内存中,所以它的读写性能非常的高。
Redis使用SDS(simple dynamic string,简单动态字符串)作为默认的字符串表示。
SDS 具有以下优点:
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
初始化一个字符串”Redis“,SDS的结构中记录了几个信息:
空间预分配
当对SDS进行扩展(比如拼接一个字符串)时,SDS会检查自身空间是否满足需要,否则会扩充自身空间,再进行拼接的操作,同时SDS在扩充空间时还会预分配多余的空间(free)来满足未来需要。
预分配规则:
len
属性的值)将小于 1 MB
, 那么程序分配和 len
属性同样大小的未使用空间, 这时 SDS len
属性的值将和 free
属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len
将变成 13
字节, 那么程序也会分配 13
字节的未使用空间, SDS 的 buf
数组的实际长度将变成 13 + 13 + 1 = 27
字节(额外的一字节用于保存空字符)。1 MB
, 那么程序会分配 1 MB
的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len
将变成 30 MB
, 那么程序会分配 1 MB
的未使用空间, SDS 的 buf
数组的实际长度将为 30 MB + 1 MB + 1 byte
。惰性空间释放
在SDS进行缩短字符串操作时,Redis不会马上把空闲的内存回收,而是将这些字节用free记录起来,等待未来使用。
当字符串未来需要进行拼接操作时,不需要去申请新的空间,而是复用记录在free中的空间:
链表被广泛用于实现 Redis 的各种功能, 比如列表键, 发布与订阅, 慢查询, 监视器, 等等。
Redis链表特点:
listNode:
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
list:
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
} list;
多个listNode和指针组成了双向链表:
List使用场景:
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
table属性是一个数组,数组中每个元素都是指向dictEntry的一个指针:
dictEntry实现:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
字典实现:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
type
属性是一个指向 dictType
结构的指针, 每个 dictType
结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。privdata
属性则保存了需要传给那些类型特定函数的可选参数。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[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。
Redis计算哈希值和索引值的方法如下:
// 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
// 使用哈希表的 sizemask 属性和哈希值,计算出索引值
// 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表:
Redis为了将哈希表的负载因子维持在一个合理的范围内,会对哈希表进行扩展和收缩操作,这些操作通过rehash来完成:
ht[1]
哈希表分配空间:ht[1]
的大小为第一个大于等于 ht[0].used * 2
的 2^n (2
的 n
次方幂);ht[1]
的大小为第一个大于等于 ht[0].used
的 2^n 。ht[0]
中的所有键值对 rehash 到 ht[1]
上面。ht[0]
包含的所有键值对都迁移到了 ht[1]
之后 (ht[0]
变为空表), 释放 ht[0]
, 将 ht[1]
设置为 ht[0]
, 并在 ht[1]
新创建一个空白哈希表, 为下一次 rehash 做准备。以下是哈希表渐进式 rehash 的详细步骤:
ht[1]
分配空间, 让字典同时持有 ht[0]
和 ht[1]
两个哈希表。rehashidx
, 并将它的值设置为 0
。ht[0]
哈希表在 rehashidx
索引上的所有键值对 rehash 到 ht[1]
, 当 rehash 工作完成之后, 程序将 rehashidx
属性的值增一。ht[0]
的所有键值对都会被 rehash 至 ht[1]
, 这时程序将 rehashidx
属性的值设为 -1
。因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0]
和 ht[1]
两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。
另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1]
里面, 而 ht[0]
则不再进行任何添加操作: 这一措施保证了 ht[0]
包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
整数集合的结构如下:
每当添加到整数集合的一个新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级, 然后才能将新元素添加到整数集合(集合升级后无法降级):
下面要将65535(int32)添加到以下集合:
因为每个 int32_t 整数值需要占用 32 位空间, 所以在空间重分配之后, 底层数组的大小将是 32 * 4 = 128 位:
将前面三个元素按顺序移动到新的位置上,过程中需要维持底层数组的有序性质不变:
最后将新元素移动到新数组上:
完成操作后的整数集合如下:
特点:
跳跃表属性:
节点属性:
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
每次创建一个新跳跃表节点的时候, 程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的“高度”。
因为节点的层数是随机生成的,所以插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。
查询节点的排名
在跳跃表中查找分值为 3.0 、 成员对象为 o3 的节点时, 沿途经历的层: 查找的过程只经过了一个层, 并且层的跨度为 3 , 所以目标节点在跳跃表中的排位为 3 。
在跳跃表中节点按分数数值从小到大排序,如果分值相同则用字典序顺序从小到大排序:
**连锁更新:**将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的表头节点,因为 e1 的 previous_entry_length 属性仅长 1 字节, 它没办法保存新节点 new 的长度, 所以程序将对压缩列表执行空间重分配操作, 并将 e1 节点的 previous_entry_length 属性从原来的 1 字节长扩展为 5 字节长。
将Redis进程的数据快照存储到硬盘
以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令
其流程如下:
写入模式:
no
:不保存,效率最高,同步交给操作系统,间隔较长everysec
:原则上每一秒钟保存一次,效率较高,只会丢1秒数据always
:每执行一个命令保存一次,效率最差,最安全