简单动态字符串

Redis没有直接使用C语言中的以空字符结尾的字符串。而是自己构建了一种名为简单动态字符串(SDS)的抽象类型。
Redis中直接使用C字符串的地方仅仅是那些不需要对字符串进行修改的地方,作为字面量使用。
Redis底层使用了大量的SDS。
比如set msg "hello world",这个命令会建立一个键值对,Redis会为msg创建一个SDS,为’hello world’创建一个键值对。

SDS的定义

1
2
3
4
5
struct sdshdr{
int len; //记录buf中已经使用的字节数,等于SDS所保存的字符串的长度
int free; //记录buf中空闲的字节的数量
char buf[]; //字节数组,用于保存字符串
}

SDS也遵循了C语言字符串以空字符结尾的惯例
K1syu9.png

SDS与C字符串的区别

SDS的优势:

  1. 常数复杂度获取字符串的长度。
  2. 可以杜绝缓冲区溢出
    C语言因为本身不记录自身长度,很容易就可能照成缓冲区溢出。比如char *strcat(char *dest,const char *src),如果没有对dest分配足够的空间,那么很容易就会出现缓冲区溢出。而SDS的api需要对SDS进行修改的时候会首先检查空间是否足够,完全杜绝了溢出的可能。
  3. 减少修改字符串时带来的内存重分配次数
    C字符串的长度和其所使用的空间总是存在len+1的关系,所以在对C字符串进行长度修改的时候,都需要重新分配空间。而SDS的空间分配策略(空间预分配,惰性空间释放)使得不需要每次都需要进行内存的重新分配。
  4. 二进制安全
    因为C字符串使用空字符作为字符串的结束符,也就是说,C语言字符串是不能包含空字符的,这种限制也使得C字符串只能保存以某种编码的文本。SDS api都是二进制安全的,所有的SDS api都会以处理二进制的方式来处理SDS存放在buf中的数据,程序不会对数据做任何限制。
  5. 兼容部分的C函数
    SDS依然遵循C语言字符串的一空字符结尾的规定,所以SDS也可以重用string.h库中定义的函数
    KtM0ij.png

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且根据通过增删节点来灵活地调整链表地长度。

在Redis中许多地方都使用到了链表,比如列表键的底层实现。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会用链表作为列表键底层实现。

链表的定义

1
2
3
4
5
typedef struct listnode{
struct listNode *prev;
struct listNode *next;
void *value; //节点的值
}listNode;

使用多个listNode来组成链表,使用adlist.h/list来持有链表

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct list{
listNode *head;
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}

Redis实现的链表有以下几个特点:

  1. 双向
  2. 无环
  3. 带表头指针和表尾指针
  4. 带链表长度计数器
  5. 多态

字典

字典是一种用于保存键值对的抽象数据结构。
字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值。

当我们在数据库中执行:
SET msg "hello world",Redis就会创建一个键为“msg”值为“hello world”的键值对。这个键值对就保存在代表数据库字典里面。
除了用于保存数据库之外,字典还是哈希键的底层实现之一。

定义与实现

Redis的字典使用的哈希表由dict.h/dictht结构定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//hash表定义
typedef struct dictht{
//hash表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;

//哈希表节点定义
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
};
//指向下一个哈希表节点
struct dictEntry *next;
}dictEntry;

KtYEDI.png

Redis中字典的定义

1
2
3
4
5
6
7
8
9
typedef struct dict{
dictType *type;
//私有数据
void *privdata;
//哈希表
dicht ht[2];
//rehash索引,当rehash不在进行时,值为-1
int trehashidx;
}

其中type属性时一个指向dictType结构的指针,每个dictType结构保存着操作用于操作特定类型的键值对的函数。
privadata属性则保存着需要传递给那些类型特定函数的可选参数
ht属性包含两个项的数组,数组中的每个项都是一个dictht哈希表,平时只使用ht[0],ht[1]在rehash时使用。

KtUNIP.png

解决键冲突:
Redis的哈希表采用链地址法来解决键冲突,每个节点都有一个next指针,被分配到同一个索引的节点通过单向链表连接起来,以此来解决键冲突问题。

rehash
为了让哈希表的负载因子维持在一个合理的范围,当哈希表中保存的简直对数量太多或太少的时候,程序需要对hash表的大小进行相应的拓展或收索。
Redis的哈希表执行rehash的步骤:

  1. 为ht[1]分配空间
  2. 将ht[0]中的键值对重新计算hash值和索引值,然后键hash值放置在ht[1]哈希表的指定位置上。
  3. 释放ht[0],将ht[1]设置为ht[0],并在ht[1]创建一个空白的哈希表。

哈希表的拓展和收索
当以下的条件任意一个被满足的时候,程序会自动开始对hash表执行拓展命令

  1. 服务器没有在执行BGSAVE或REWRITEAOF命令,且负载因子大于等于1
  2. 服务器正在执行BGSAVE或REWRITEAOF命令,且负载因子大于等于5

负载因子的计算公式:load_factor=ht[0].used/ht[0].size
当哈希表的负载因子小于0.1,程序会自动开始对哈桑表进行收索操作

渐进式rehash
rehash动作并不是一次性的,集中式地完成地,而是分多次、渐进式地完成地。因为如果一次性rehash太多地键值对,那么庞大地计算量可能会导致服务器在一段实践内停止服务。
渐进式hash,是通过hash表中地rehashidx属性记录rehash进度的。

在渐进式hash的过程中对hash表进行操作的话,查找首先会在ht[0]中查找,如果没有找到再去ht[1]中查找。新添加的键值对都会被保存在ht[1]中。

跳跃表

跳跃表是一种有序的数据结构,它通过在每个节点中维护多个指向其它节点的指针,从而达到快速访问节点的目的。
Redis使用跳跃表来作为有序集合的底层实现之一。
KtDKPI.png

KtDKPI.png

跳跃表的定义与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//跳跃表节点
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}zskiplistNode;

跳跃表节点的level节点数组可以包含多个元素,每个元素都包含了一个指向其它节点的指针。程序可以通过这些层来加快访问其它节点的速度。
每个层都有指向表尾方向的前进指针,用于从表头项表尾方向访问节点。
跨度,层的跨度用于记录两个节点之间的距离。两个节点之间的跨度越大,它们相距的就越远。指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何的节点。跨度实际上是用来计算排位的。

1
2
3
4
5
6
7
typedef struct zskiplist{
struct skiplistNode *header,*tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}

整数集合

整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合元素的数量不够多时,Redis就会使用整数集合作为集合键的底层实现。

整数集合的设计与实现

1
2
3
4
5
6
7
8
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint_32_t length;
//保存元素的数组
int8_t contents[];
}intset;

KNceWd.png

升级:
当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型还要长的时候,整数集合需要先进行升级。
升级主要分为三个步骤:

  1. 根据新元素的类型,拓展整个整数集合底层数组的空间大小,并未新元素分配空间。
  2. 将底层数组现有的所有元素都转化为与新元素相同的类型,并将转化后的类型放到正确的位置(保持有序)。
  3. 将新元素添加到底层数组里面。

整数集合是不支持降级的

升级的好处:

  1. 提升灵活性
    C语言是静态类型语言,我们通常不会把两种不同类型的值放在同一个数据结构里面。
  2. 节约内存
    整数集合在确保可能存放多种不同类型的值的同时,确保升级操作只在有需要的时候进行。

压缩列表

压缩列表是列表键和哈希键的底层实现之一。当一个列表键字包含少量的列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

压缩列表的定义与实现

压缩列表是Redis为了解决内存而开发的。是由一系列特殊编码的连续内存块组成的顺序型数据结构。

previous_entry_length
以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性长度可以是1字节或5字节。
encoding
记录了节点content属性所保存数据的类型以及长度。
content
节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值得类型和长度由节点encoding属性决定。

对象

Redis并没有直接使用之前提到的简单数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含了字符串对象,列表对象,哈希对象,集合对象和有序集合对象五种类型的对象。

对象类型和编码

Redis使用对象类表示数据库中的键和值,每当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个是键对象,一个是值对象。

1
2
3
4
5
6
7
8
9
//redis的每一个对象都是由一个redisObject结构表示的
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
}

ptr指向对象的底层实现数据结构,这些数据结构由对象的encoding属性决定。encoding属性记录了对象所使用的编码,也就是说对象使用了什么数据结构作为对象的底层实现。
KaiGnJ.png

每种类型的对象都至少使用了两种不同类型的编码。
KaiUtx.png
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提高了redis的灵活性和效率。因为Redis可以根据不同的使用场景来为对象设定不同的编码,从而优化了对象在某一场景下的效率。

字符串对象

字符串对象的编码可以是int,row,embstr.
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型表示,那么字符串对象会值保存在字符串对象数据结构中ptr属性里,并将编码类型设置为int。

如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于32字节,那么这个对象将使用SDS来保存这个字符串值,并将对象编码设置为row。

如果字符串对象保存的是一个字符串值,且这个字符串值的长度小于等于32字节是,那么字符串对象将使用embstr编码的方式来保存这个字符串值。embstr编码也会使用SDS,只不过它通过一次内存分配获取一片连续的空间,就实现了redisObject 和sdshdr结构内存的分配。而row编码,需要两次.一次用来分配redisObject一次用来分配sdshar。而且因为Redis中没有embstr编码的修改程序,也就说其实embstr编码是只读的,写操作会重新分配。
通过embstr编码创建的内存块的结构:
KaAY7j.png

值得注意的是,可以用long double表示的浮点数,在redis中也是作为字符串值来存放的

列表对象

列表对象的编码可以是ziplist或linkedlist。
ziplist编码的列表使用压缩列表来作为底层实现,每个压缩列表节点保存一个列表节点。
另一个方面,linkedlist编码的列表对象使用双端链表作为底层实现。每个双端链表都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。双端列表结构中可以嵌套多个字符串对象。
编码转换
当列表对象同时满足以下两个条件时,列表对象可以使用ziplist。

  1. 列表保存的字符串元素都是小于列表
  2. 列表对象保存的元素数量时小于512个的。

哈希对象

哈希对象的编码可以是ziplist或者hashtable。
ziolist编码的哈希对象实现,是将键和值同时一次推入压缩列表表尾。
hashtable编码的哈希表实现使用字典作为底层实现,哈希对象的每个键值对都使用一个字典键值对来保存。字典的键和值都是字符串对象

编码转换:
当哈希对象可以同时满足以下两个条件时,哈希对象使用siplist编码:

  1. 哈希对象所保存的键和值的字符串长度都是小于64字节的。
  2. 哈希对象所保存的键值对数量是小于512个的。

集合对象

集合对象的编码可以是intset或者hashtable。
intset编码的集合使用整数集合作为底层实现,集合对象包含的而所有元素都保存在整数集合中。

hashtable编码的集合对象使用字典作为底层实现,字典的每个值都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值都被全部设置为null。

编码的转换:
当集合的对象满足以下两个条件时,可以使用intset编码。

  1. 集合对象保存的所有元素都是整数值
  2. 集合对象保存的元素数量不超过512个

有序集合对象

有序集合对象的编码可以是ziplist或者skiplist。
ziplist使用压缩列表来作为底层实现,集合中的每个元素使用两个挨在一起的列表节点来博爱从,第一个节点保存成员,第二个节点保存分数。压缩列表按分值从小到大进行排序。

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构结构同时包含了一个字典和一个跳跃表。

1
2
3
4
5
typedef struct zset{
zskiplist *zsl;
dict *dict;

}zset;

zset中的zsl跳跃表按分值的大小保存了从小到大所有集合元素,每个跳跃表界定啊都保存了一个集合元素。跳跃表节点的object属性保存了元素的成员,而跳跃表界定啊的score属性保存了元素的分值。通过跳跃表可以非常方便的实现有序集合的范围性操作
而zset中的dist属性保存了有序集合中成员到分数的映射,可以以常数复杂度查找给定成员的分数。

编码的转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  1. 有序集合保存的元素数量小于128个
  2. 有序集合中所有元素的成员都小于64字节。

内存回收

Redis实现了自己的引用计数计数,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候释放对象并进行内存回收。每个对象的引用计数信息,由redisObject结构的refcount属性记录。

引用计数信息的变化:

  1. 当创建一个对象时,引用计数的值会被初始化为1
  2. 当一个对象被一个新的程序使用时,它的引用计数值会被增一
  3. 当对象不再被一个程序使用时,它的引用计数值会被减一。
  4. 当对象的引用计数值为0时,对象所占用的内存会被释放。

KabneS.png

对象共享

对象的引用计数还可以实现对象共享。
KabRTe.png

redis不会共享包含字符串的对象

对象的空转时长

redisObject结构中还包含了一个lru属性,该属性记录了对象最后一次被命令程序访问的时间。
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过maxmemory选项设置的上限值时,空转时长较高的那部分会被优先释放。

服务器中的数据库

Redis服务器的所有状态都保存在redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库。

1
2
3
4
5
6
7
8
struct redisServer{
//...
//一个数组,保存服务器中的所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
//..
}

在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库。dbnum的值有服务器配置的database选项决定,默认情况下,该选项的值为16.

切换数据库

每个redis客户端都有自己的目标数据库,默认情况下redis客户端的默认数据库为0号数据库,用户可以通过select命令来切换数据库。客户端状态的redisclinet结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisdb结构的指针。

1
2
3
4
typedef struct redisClient{
//记录客户端当前正在使用的数据库
redisDb *db;
}redisClient;

通过修改db指针的值,让它指向服务器中不同的数据库。

KajvQA.png

数据库键空间

服务器中每个数据库都由一个redisDb结构表示,其中redisDb结构的dict字典保存了数据库中所有键值对,我们将这个字典称为键空间。

1
2
3
4
5
typedef struct redisDb{
//。。。
//数据库键空间
dict *dict;
}redisDb;

键空间的键也就是数据库的键,每个键都是一个字符串对象。
键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象。
KwEA2t.png

添加键、删除键,更新键
添加键和删除键其实就是操作键空间中的键值对对象。

对键取值
对键取值就是在在键空间中去除键所对应的值对象。

读写键空间时的维护操作
当使用Redis命令对数据库进行读写时,数据库不仅会对键空间执行制定的读写操作,还会执行一些额外的维护操作:

  1. 在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中和键不命中次数
  2. 在读取一个键之后,服务器会更新键的LRU(最后一次使用时间),这个值可以计算键的闲置时间。
  3. 如果服务器在读取一个键时,发现这个键已经过期了,那么它会首先删除这个键。
  4. 如果客户端使用WATCH监视一个键时,如果被监视的对象被修改的话,这个键会被标记为脏。
  5. 服务器每修改一个键之后,都会对脏键计数器的值增一,这个值会触发服务器的持久化以及复制操作。
  6. 如果服务器开启了数据库通知,那么对键修改之后,服务器将按配置发送相应的数据库通知。

设置键的生存时间或过期时间:
通过EXPLRE命令或PEXPIRE命令,客户端可以以秒或毫秒为单位为数据库中的某个键设置生存时间(Time to live,TTL)。

1
2
3
4
5
EXPLRE <key> <ttl> //将键的生存时间设置为ttl秒
PEXPIRE <key> <ttl> //将键的生存时间设置为ttl毫秒
EXPIREAT <key> <timestamp> //将key的过期时间设置为timestamp所指定的秒数时间戳

PEXPIREAT <key> <timestamp> //将键的过期时间设置为timestamp所指定的毫秒数时间戳

无论在客户端中使用的是哪一种命令,最终的执行效果都和PEXPIREAT命令一样
KwBO78.png

保存过期时间
redisDb结构中的expires字典保存了数据库中所有键的过期时间,我们称之为过期字典。
过期字典的键是一个指向键空间中某个对象的指针,而过期字典的值是一个long类型的整数。
KwrfRH.png

过期键删除策略

  1. 定时删除,在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
    它能及时地删除过期地键,但是对CPU时间是最不友好地。
  2. 惰性删除,放任键的过期时间,只是从键空间取键时,检查是否过期,如果发现过期就进行删除。
    对CPU是最友好的,但是对内存是不友好的。过期键可能长时间的占用内存。
  3. 定期删除:每隔一段时间,程序就会对数据库进行一次检查,删除里面的过期键。
    定时删除是对前两种方案的一种折中。

过期键对AOF,RDB和复制功能的影响

RDB
在生成RDB文件的时候,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。

当载入RDB文件的时候,如果服务器以主服务器运行,那么程序会对文件中的键进行检查,未过期的键会被载入数据库中。
如果服务器以从服务器模式运行,那么载入RDB文件时,文件中保存的所有键,无论过期与否都会被载入。
AOF
如果数据库中的某个键已经过期了,但是它还没有被惰性删除或定期删除,那么AOF将不会受任何的影响。当过期的键被惰性删除或定期删除之后,程序会向AOF文件中追加一条DEL命令,来显示的指明该键已被删除。
当在进行AOF重写的时候,程序会对键进行检查,已过期的键不会被保存到重写后的AOF文件中。

复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制。

  1. 当主服务器在删除一个过期键之后,会显示地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
  2. 从服务器在处理读命令时,即使发现了键过期了,也会当作没过期来进行处理。
  3. 从服务器只在收到主服务器发来地DEL命令之后,才会删除过期键。

RDB持久化

RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态(指服务器中的非空数据库以及它们的状态)保存到已RDB文件中。RDB持久化生成的是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。

服务器在启动时候会主动取检测RDB文件,然后自动载入,但是如果服务器开启了AOF持久化功能,那么优先使用AOF文件来还原数据库状态。只有在AOF持久化关闭的时候才会使用AOF来进行数据库的还原。

值得注意的时候BGREWRITEAOF和BGSAVE同一时间只能有一个在执行,有些是出于避免竞争条件的考虑,有些是出于性能的考虑。

Redis服务启动的时候,用户可以通过配置文件或者传入启动参数的形式设置save选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct redisServer{
//...
//记录保存条件的数组
struct saveparam *saveparams;
//修改计数器,据上一次save或bgsave后的修改次数
long long dirty;
//上一次执行保存的时间
time_t lastsave;

}

struct saveparam{
//秒数
time_t seconds;
//修改数
int changes;

}

RDB文件结构

RDB文件所保存的是二进制数据。
KDmsRe.png
开头的REDIS部分长度为5个字节,用来检测文件是否是RDS文件。
db_version:长度为4字节,记录了文件的版本号。
databases:包含着零个或任意多个数据库,以及各个数据库中的键值对数据。
EOF:长度为1字节,标识RDB文件的正式结束。

每个非空数据库又包含3个部分:
KDuZBn.png
其中SELECTDB的值为一字节,当程序读到这个值的时候,它就知道接下来是数据库号码了。
每个key_value_pairs也是由三部分组成的。
KDu03D.png

AOF持久化

AOF持久化是通过保存服务器所执行的写命令来记录数据库状态的。
KDQ3jg.png
被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,Redis请求协议是纯文本的。
在服务器启动的时候,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。

AOF持久化的实现

AOF持久化功能的实现可以分为命令追加,文件写入,文件同步。
当服务器在执行完写命令后,会以协议格式见被执行的命令写入到aof_buf中

1
2
3
4
5
6
struct redisServer{
//...
//AOF缓冲区
sds aof_buf;

}

通过配置服务器:appendfsync:

  1. always:将aof_buf缓冲区中的所有内容写入并同步到AOF文件
  2. everysec:将aof_buf缓冲区中的内容写入到文件,如果上次写入超过一秒种,就再次对AOF进行同步。这个同步是由一个线程专门负责。
  3. no:aof_buf缓冲区中的内容写入文件,并从不主动同步,由操作系统决定。

这个地方的写入是指,调用系统的write写入到缓冲区,同步是将缓冲区写入到磁盘

现代OS中调用write函数首先是写入到缓冲区中,等待一定的时机,才由缓冲区写入磁盘,只有当写入磁盘了才算真正的持久化了

AOF重写

随着服务器的运行,AOF文件中的内容也会越来越多。这个时候,就需要使用Redis的AOF重写功能。它会创建一个新的AOF文件来代替原来的AOF文件,且新的AOF文件中不包含冗余的命令。AOF重写是不会依赖于旧的AOF文件的,而是通过服务器的当前状态来实现的。因此新的AOF文件只包含还原当前状态的必须命令。
Redis使用子进程对AOF进行重写,在这期间执行的命令可能改变数据库的状态,所以Redis引入了一个AOF重写缓冲区。

在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

  1. 执行客户端发来的命令
  2. 将执行后的写命令追加到AOF缓冲区
  3. 将执行后的写命令追加到AOF重写缓冲区。

从创建子进程起,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
KDNbVO.png

事件

Redis服务器是一个事件驱动程序,服务器需要处理两类事件:

  1. 文件事件,文件事件就是服务器套接字操作的抽象,服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列的通信操作。
  2. 时间事件,时间事件就是对定时操作的抽象。

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器。文件事件是对套接字操作的抽象,每次套接字变为可应答,可写或者可读时,相应的文件事件就会产生。
文件事件处理器使用IO多路服务程序来同时监听多个套接字。
KDDX01.png
尽管多个文件事件可能会并发的出现,当IO多路复用程序总是会将所有产生的事件的套接字放到一个队列里面,然后通过这个队列,以有序的、同步的、每次执行一个套接字的方式向文件事件分派起传送套接字。

KDrthT.png
Redis的IO多路复用程序有多个IO多路复用库实现可选。
KDr6N6.png

IO多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那么事件分派器会优先处理AE_READABLE事件。

时间事件

Redis时间时间又可以细分为定时事件和周期性事件。
一个时间时间通过上个属性来描述:

  1. id,服务器会为每个时间时间创建一个全局唯一ID,ID从小到大递增。
  2. when:毫秒精度的unix时间戳。
  3. timeproc:时间处理函数,一个函数。

一个时间事件是定时事件还是走起周期性事件是由timeproc的返回值决定的,如果返回的是AE_NOMORE, 那么这个事件是定时事件,事件到达一次后,就会被删除,如果返回的不是AE_NNOMORE那么就是一个周期性事件,每次事件到达后都会更新when。

Redis将所有的事件事件都放置在一个无序链表(指不是按when排序的)中,每当时间事件执行器运行的时候,它就遍历整个链表,查找所以已达到(when>=当前时间)的时间事件,并调用相应的事件处理器。
KsDxhV.md.png

事件处理角度下的服务器运行流程:
Kss6Zd.png

Redis客户端

Redis通过使用由IO多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

服务器为每个客户端都建立了一个redisClinet结构,整个结构保存了客户端当前的状态信息,以及执行相应的功能许哟啊用到的数据结构。
KsgR0g.md.png]
在redisServer中保存了一个指向由客户端状态结构组成的链表的指针。

客户端的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct redisClient{
//...
int fd; //套接字描述符
robj *name; //客户端名字
int flags;//客户端标准属性
sds querybuf;//输入缓冲区
robj **argv; //命令参数
int argc;//命令参数数量
struct redisCommmand *cmd;//命令的实现函数

/*输出缓冲区*/
char buf[REDIS_REPLY_CHUNK_BYTE]; //固定大小输出缓冲区
int bufpos; //固定大小输出缓冲区的实际使用长度
list *reply; //可变大小输出缓冲区

int authenticated; //身份认证属性,1为通过认证,0为未通过认证

time_t ctime; //创建客户端的时间
time_t lastinteraction; //最后一次与服务器互动的时间
tine_t obuf_soft_limit_reached_time; //输出缓冲区第一次到达软性限制时间
}redisClient;

套接字描述符fd

根据客户端类型的不同,fd属性的值可以是-1或者大于-1的整数。

  1. 伪客户端:fd的值为-1.命令的来于AOF文件或者Lua脚本,而不是网络,这种客户端不需要设置套接字连接。
  2. 普通客户端:fd的值大于-1的整数,使用套接字来与服务器进行通讯。

客户端名字
默认情况下,客户端是没有名字的,也就是所name是指向null的。如果设置了名字,男儿name就指向保存了名字的对象。

客户端标志属性
客户端标志属性flags记录了客户端的角色,以及客户端目前所处的状态。flags属性的值可以是单个标志也可以是多个标志的二进制或。如:
flags=<flag>
flags=<falg1>|<flag2>|<flag3>

常见的表示角色的客户端:

  1. REDIS_MASTER表示客户端代表的是一个主服务器。
  2. REDIS_SLAVE表示客户端代表的是一个从服务器。
  3. REDIS_LUA_CLIENT表示客户端专门用于处理Lua脚本中包含Redis命令的伪客户端。
  4. REDIS_MONITOR表示客户端正在执行MONITOR命令。
  5. REDIS_UNIX-SOCKE表示服务器使用UNIX套接字来连接客户端。

输入缓冲区querybuf
客户端状态的输入缓冲区用于保存客户端发送的命令请求。输入缓冲区的大小会根据输入内容动态地缩小或者扩大,但他地最大大小不能超过1GB,否则服务器将会关闭这个客户端。

命令与命令参数
服务器将客户端发送来的命令保存在客户端状态的querybuf属性中,之后就会对命令的内容进行分析,并将得出的命令参数以及命令参数的个数,
argv属性是一个数组,数组中的每个元素都是一个字符串对象,其中argv[0]是要执行的命令,其后的argc-1项都是传给该命令的参数。
Ks5g8H.png
在该图中的例子中,argc==3,因为set本身也是一个参数

命令实现函数
cmd,当服务器完成协议内容的解析得到argv和argc后,服务器就会根据argv[0]知道对应的实现函数。寻找对应的命令实现函数需要借助一个字典。这个字典的键位sds结构,保存命令的名称,字典的值是一个rediscommand结构,该结构保存了实现函数、命令的标记,命令应该给定的参数个数、命令总执行次数和总消耗时长等统计信息。
Ks7Ess.png

输出缓冲区
执行命令所得到命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用。
固定大小的已被用于保存那些比较短的回复;可变大小缓冲区主要保存那些长度比较大的回复。
KsHe6H.png

KsHM7t.png

身份认证
客户端状态的authenticated属性记录了客户端是否通过了身份验证。0为未通过认证,1为通过了认证。当authenticated属性的值为0时,除了执行auth命令,其余所有命令都会被拒绝。
authenticated仅在服务器启用了身份认证功能之后,才能生效

时间
ctime属性记录了客户端创建的时间,
lastinteraction属性记录了客户端与服务器最后一次进行互动的时间。该属性可以用来计算客户端的空转时间。
obuf_soft_limit_reached_time属性记录了输出缓冲区第一次到达软性限制的时间。

客户端的创建与关闭

创建普通客户端

如果客户端是通过网络与服务器进行连接的普通客户端,那么在客户端使用connect函数连接服务器的时候,服务器就创建了连接事件处理器,为客户端创建相应的客户端状态,并将这个客户端状态添加到服务器状态结构clinets链表尾。

关闭普通客户端

照成普通客户端关闭的原因:

  1. 客户端进程被杀死,或网络连接关闭。
  2. 客户端向服务端发送了不符合协议格式的请求。
  3. 客户端称为了CLIENT KILL命令的目标
  4. 服务器端配置了timeout选项,当客户端的空转时间超过限制,就会被服务器端关闭。
  5. 客户端发送的请求超过了输入缓冲区的限制(1GB)
  6. 要发送给客户端的命令回复的大小超过了输入缓冲区的限制大小。

为了避免客户端的回复过大,服务器会检查输出缓冲区的大小,服务器有两种模式来限制客户端的输出缓冲区的大小:

  1. 硬性限制,输出缓冲区的大小达到限制的大小,服务器立刻关闭客户端。
  2. 软性限制,如果输出缓冲区的大小达到限制的大小后,记录达到软性限制的起始时间,如果缓冲区大小一直超出软性限制(超过设定的时长),那么服务器将关闭客户端。

Lua伪客户端。

服务器在创建的时候就会创建一个用于执行Lua脚本中的redis命令的客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中。
直到服务器关闭才会关闭Lua伪客户端。

AOF文件的伪客户端

当服务器载入AOF文件时,会创建AOF文件的伪客户端,当载入完成,就会关闭这个伪客户端。