八、数据库
8.1 服务器中的数据库
Redis服务器将所有数据库状态保存在redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb代表一个数据库;
struct redisServer {
// ...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
//服务器数据库数量,初始化时决定创建多少个数据库
int dbnum;
}
客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,是指向一个redisDb结构的指针。
typedef struct redisClient {
//记录客户端当前正在使用的数据库,可以切换
redisDb *db;
}
8.2 数据库键空间
redisDb的dict字典保存了数据库中的所有键值对,我们称该字典为键空间;
typedef struct redisDb {
//键空间,保存所有键值对
dict *dict;
}redisDb;
8.3 键的生存时间或过期时间
1、键过期时间保存
typedef struct redisDb {
//过期字典,保存键的过期时间
dict *expires;
}redisDb;
设置过期时间:
- EXPIRE
- PEXPIRE
- EXPIREAT
- PEXPIREAT
移除过期时间:
PERSIST
8.4 过期键的删除策略
Redis采用惰性删除和定期删除两种策略相结合的方式;
1、惰性删除策略
程序在取键是才进行过期检查,若过期则删除;
2、定期删除策略
每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
8.5 AOF、RDB和复制功能对过期键的处理
1、AOF
AOF文件写入:当过期键被惰性或定期删除后,程序会向AOF文件追加一条DEL命令来记录该键已被删除;
举个例子:客户端的GET message命令获取过期message键,则服务器会有三个动作:
- 从数据库删除message键;
- 追加一条DEL message命令到AOF文件;
- 向执行GET命令的客户端返回空回复;
AOF重写:程序会对数据库中的键进行检查,过期的键不会保存到重写的AOF文件中;
2、RDB
生成RDB文件:和AOF重写类似,已过期的键不会被保存到新创建的RDB文件中;
载入RDB文件:
- 主服务器模式:过期键会被忽略;
- 从服务器模式:不论是否过期,都会被载入数据库;
3、复制
- 主服务器:删除一个键后会向所有从服务器发送DEL命令;
- 从服务器:执行客户端命令时,即使键过期也不会删除,而是正常返回键值,只有接收到主服务器的DEL命令,才会删除该键;
第二部分 单机数据库的实现
九、RDB持久化
9.1 RDB文件的创建与载入
创建:
redis> SAVE #等待直到RDB文件创建完毕
OK
redis> BGSAVE #派生子进程,由子进程创建RDB文件
Background saving started
载入过程:
9.2 自动间隔性保存
可以通过配置save选项,让服务器每隔一段时间就自动保存一次BGSAVE命令;
save 900 1 # 服务器在900秒内,对数据库进行了至少1次修改
save 300 10
save 60 10000
之后会设置服务器状态redisServer结构的saveparams属性:
struct redisServer {
//记录了保存条件的数组
struct saveparam *saveparams;
//上次成功执行SAVE命令或BGSAVE命令后,服务器进行了多少次修改
long long dirty;
//上次执行保存的时间
time_t lastsave;
}
struct saveparam {
//秒数
time_t seconds;
//修改数
int changes;
}
9.3 RDB文件结构
完整的RDB文件结构:
- REDIS为固定字符串;
- db_version为版本号;
- databases包含0~多个数据库;
- EOF标志文件正文结束;
- check_sum保存校验和;通过前面四个字段计算得出,用于检查文件是否有出错或损坏;
databases部分
key_value_pairs部分
不带过期时间的键值对:
带过期时间的键值对:(键值对过期了才会带上过期时间)
TYPE:记录了value的类型,长度为1字节,值可以是以下常量其中之一
REDIS_RDB_TYPE_STRING
REDIS_RDB_TYPE_LIST
REDIS_RDB_TYPE_SET
REDIS_RDB_TYPE_ZSET
REDIS_RDB_TYPE_HASH
REDIS_RDB_TYPE_LIST_ZIPLIST
REDIS_RDB_TYPE_SET_INTSET
REDIS_RDB_TYPE_ZSET_ZIPLIST
REDIS_RDB_TYPE_HASH_ZIPLIST
key:字符串对象,编码方式和REDIS_RDB_TYPE_STRING类型的value一样;
value:根据TYPE类型不同以及保存内容长度不同,value的结构和长度也会不同;
EXPIRETIME_MS:告知读入程序,接下来读入的是一个以ms为单位的过期时间;
ms:记录一个以ms为单位的UNIX时间戳
9.4 RDB文件分析
使用od命令分析RDB文件,给定-c参数可以以ASCII编码方式打印输入文件,-x参数可以以十六进制方式打印输入文件
1、不包含任何键值对的RDB文件
创建一个数据库状态为空的RDB文件后以od命令打印RDB文件:
$ od -c dump.rdb
因为不包含任何数据库数据,所以RDB文件由下面四个部分组成:
- 五字节的"REDIS"字符串;
- 四字节的版本号:0006;
- 一字节的EOF常量:337;
- 八字节的校验和:334 … V
2、包含字符串键的RDB文件
redis> SET MSG "HELLO"
OK
redis> SAVE
OK
- 五字节的"REDIS"字符串;
- 四字节的版本号:0006;
- 376 \0:切换到0号数据库(376为特殊值SELECTDB);
- \0 003 M S G 005 HELLO:\0 为TYPE(不同的TYPE值不同),003代表MSG的长度,005为HELLO的长度
- 一字节的EOF常量:337;
- 八字节的校验和:207 … 343
3、带有过期时间的字符串键的RDB文件
redis> SETEX MSG 10086 "HELLO"
OK
redis> SAVE
OK
- 五字节的"REDIS"字符串;
- 四字节的版本号:0006;
- 376 \0:切换到0号数据库(376为特殊值SELECTDB);
- 374:代表特殊值EXPIRETIME_MS。
- \ 2 …001 \0 \0:代表8字节长的过期时间
- \0 003 M S G 005 HELLO:\0 为TYPE(不同的TYPE值不同),003代表MSG的长度,005为HELLO的长度
- 一字节的EOF常量:337;
- 八字节的校验和:212 … 306
十、AOF持久化
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。
10.1 持久化实现
AOF持久化功能可分为append、文件写入、文件同步三个步骤。
1、append
服务器执行完一个命令便会将命令追加到服务器状态的aof_buf缓冲区的末尾:
struct redisServer {
// ...
// AOF缓冲区
sds aof_buf;
// ...
};
2、AOF文件的写入与同步
Redis服务器进程是一个事件循环,负责接收客户端命令、客户端发送命令回复以及定时运行的函数,最后会调用flushAppendOnlyFile函数来看看是否将aof_buf缓冲区中的内容写入和保存到AOF文件,伪代码如下:
def eventLoop():
while True:
# 处理文件事件
processFileEvents()
# 处理时间事件
processTimeEvents()
# 考虑将aof_buf中的内容写入和保存到AOF文件中
flushAppendOnlyFile()
flushAppendOnlyFile()由redis.conf中的appendsync配置值来决定。
appendsync的值 | flushAppendOnlyFile行为 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 |
everysec | 将aof_buf缓冲区中的所有内容写入AOF文件,如果上次同步事件距离现在超过1s,则再次进行同步,由子线程fork执行 |
no | 将aof_buf缓冲区中的所有内容写入AOF文件,但不对AOF文件进行同步,何时同步由操作系统决定。 |
10.2 AOF文件的载入与数据还原
10.3 AOF重写
为避免文件体育越来越大,且还原速度会变慢,以及存在冗余的命令,比如一个List键分多次放入值,需要多条指令等。
1、AOF文件重写的实现
通过直接读取服务器当前的数据库状态来实现。
举个例子:
redis> SADD animals "Cat"
(integer) 1
redis> SADD animals "Dog" "Panda" "Tiger"
(integer) 3
redis> SREM animals "Cat"
(integer) 1
redis> SADD animals "Lion" "Cat"
(integer) 1
直接读取服务器的animals键的值,就可以用一条指令SADD animals “Dog” …
2、AOF后台重写
通过子进程来执行,不过为了解决子进程进行AOF重写期间,服务器进程还在处理其他命令,可能出现数据库状态不一致,所以设置了一个AOF重写缓冲区,其作用如图所示:
子进程执行期间,服务器执行客户端的命令,会将命令同时追加到AOF缓冲区和重写缓冲区,在子进程结束后便会通知服务器将重写缓冲区的内容写入新的AOF文件并替换旧文件。
十一、事件
服务器事件分为:文件事件和时间事件
11.1 文件事件
文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字。
文件事件处理器的四个组成部分:
I/O多路复用程序结构:
11.2 时间事件
Redis的时间事件分为两类:
- 定时事件:一段程序在指定事件之后执行一次;
- 周期性事件:一段程序每隔指定时间就执行一次;
时间时间主要有三个属性:
id:服务器为时间事件创建的唯一ID,从小到大顺序递增;
when:毫秒精度的UNIX时间戳,记录事件到达时间;
timeProc:时间事件处理器,一个函数;
1、实现
时间事件会放在一个无序链表中,每当时间事件执行器运行时,就会遍历整个链表,查到已到达的时间事件就调用相应的事件处理器。
id从大到小排序,新事件在表头,when是无序的。
2、事件的调度与执行
十二、客户端
服务器为每隔客户端建立了redis.h/redisClient结构用于保存客户端当前状态信息。
struct redisServer {
// ...
// 一个链表,保存了所有客户端状态
list *clients;
// ...
};
如图所示为三个客户端连接的情况。
12.1 客户端属性
通用属性:
typedef struct redisClient {
// ...
//套接字描述符,fd = 1时是伪客户端,普通客户端 > -1
int fd;
// 客户端名称,默认为NULL,可指向一个字符串对象
robj *name;
//客户端角色以及所处状态,REDIS_MASTER标志为主服务器,REDIS_SLAVE标志位从服务器....
int flags;
//输入缓冲区:保存客户端发送的命令
sds querybuf;
//数组,每一项位一个字符串对象,argv[0]为执行的命令,之后为命令参数
robj **argv;
//argc负责记录argv数组的长度
int argc;
//argv[0]对应的redisCommand
struct redisCommand *cmd;
//输出缓冲区(固定大小缓冲区)REDIS_REPLY_CHUNK_BYTES默认为16*1024
char buf[REDIS_REPLY_CHUNK_BYTES];
//记录buf目前已使用的字节数量
int bufpos;
//可变大小的输出缓冲区(当固定缓冲区用完或存不下时使用)
list *reply;
//身份验证(值为0表示未通过验证,1代表通过验证)
int authenticated;
//创建客户端时间,用来计算客户端与服务器已连接了多少秒
time_t ctime;
//记录了客户端与服务器最后一次进行互动的时间
time_t lastinteraction;
//记录了输出缓冲区第一次到达软性限制的时间
time_t obuf_soft_limit_reached_time;
}redisClient;
redis> CLIENT list
addr=127.0.0.1:53428 fd=6 name=user age=1242 idle=12
# age记录了客户端与服务器连接了多少时间
# idle距离客户端与服务器最后一次进行互动已经过去多少秒
12.2 客户端的创建与关闭
1、创建普通客户端
每有一个客户端连接,便会将新的客户端状态添加到服务器状态结构clients链表的末尾;
2、关闭普通客户端
为避免客户端回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。服务器做了两种限制:
硬性限制:输出缓冲区大小超过了硬性限制设置的大,则关闭客户端;
软性限制:若输出缓冲区超过了软性限制,但没超过硬性限制,则会时刻健康客户端,若持续时间超过服务器设定时间,则关闭;通过client-output-buffer-limit属性值来设置;
# 普通客户端的硬性限制和软性限制都为0,表示不限制客户端的输出缓冲区
client-output-buffer-limit normal 0 0 0
# 从服务器客户端的硬性限制设置为256MB,软性限制为64MB,软性限制时长为60s
client-output-buffer-limit slave 256mb 64mb 60
# 将执行发布与订阅功能的客户端的硬性限制设置为32MB,软性限制为8MB,软性限制时长为60s
client-output-buffer-limit pubsub 32mb 8mb 60
十三、服务器
13.1 命令请求执行过程
一个命令从发到到获得回复的过程包括下述操作:
- 客户端向服务器发送命令请求;
- 服务器接收并处理,在数据库中进行设置操作,并产生回复;
- 服务器将回复发送给客户端;
- 客户端接收服务器回复并打印;
1、客户端向服务器发送命令请求
举个例子:假设用户在客户端键入命令
SET KEY VALUE
客户端会将命令转换成协议:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
2、读取命令请求
服务器调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令并保存到客户端状态的输入缓冲区querybuf;
- 对querybuf中的命令进行分析,提取命令参数以及参数个数,分别保存于argv属性和argc属性;
- 调用命令执行器,执行客户端指定命令;
3、命令回复发送给客户端
命令实现函数会将命令回复保存到客户端的输出缓冲区buf[REDIS_REPLY_CHUNK_BYTES]并为客户端套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器会执行命令回复处理器,将保存在客户端buf[REDIS_REPLY_CHUNK_BYTES]中的命令回复发送给客户端;
4、客户端接收并打印命令回复
13.2 命令执行器
命令执行器是13.1中提到的服务器用于调用客户端指定命令,其执行步骤如下:
1、查找命令实现
命令执行器由argv[0]参数在命令表中找参数指定的命令并将找到的命令保存到客户端的cmd属性;
2、执行预备操作
- 检查cmd是否指向NULL,是则不再执行并返回一个错误;
- 由cmd指向的redisCommand结构的arity属性,检查给定的参数个数是否正确,不对则不再执行并返回一个错误。例如值为-3代表参数个数必须>=3;
- 检查客户端是否已通过身份验证;
- 如果客户端正在执行事务,则服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令放进事务队列;
- …
3、调用命令的实现函数
就是执行setCommand(client)语句执行指定的操作;
4、执行后续工作
例如如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚执行的命令请求写入AOF缓冲区等等;
13.3 serverCron函数
serverCron函数默认每隔100ms执行一次,负责管理服务器资源保持服务器的良好运行;
- 更新服务器时间缓存;
- 更新LRU时钟;
- 更新服务器每秒执行命令次数;
- 更新服务器内存峰值记录;
- 处理SIGTERM信号;(用于关闭服务器)
- 管理客户端资源(连接超时、输入缓冲超过限定);
- 管理数据库资源(删除过期键等,具体可见第八节);
- 执行被延迟的BGREWRITEAOF;
- 检查持久化操作的运行状态;
- 将AOF缓冲区的内容写入AOF文件;
- 关闭异步客户端;(关闭输出缓冲区超过限制的客户端)
- 增加cronloops计数器的值;
13.4 初始化服务器
1、初始化服务器状态结构
调用initServerConfig函数,主要工作:
void initServerConfig(void) {
//设置服务器运行ID;
getRandomHexChars(server.runid, REDIS_RUN_ID_SIZE);
//为运行id加上结尾字符
server.runid[REDIS_RUN_ID_SIZE] = '\0';
//设置服务器默认运行频率;
server.hz = REDIS_DEFAULT_HZ;
//设置服务器默认配置文件路径;
server.configfile = NULL;
//设置服务器的运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
//设置默认服务器端口号
server.port = REDIS_SERVERPORT;
//设置服务器默认的RDB、AOF持久化条件
//初始化服务器的LRU时钟
}
2、载入配置选项
举例:
# 终端输入
$redis-server --port 10086
# 在配置文件修改配置,在redis.conf中修改
database 32
# 关闭RDB文件的压缩功能
rdbcompression no
3、初始化服务器数据库结构
比如:
- server.clients链表;
- server.db数组;
- ...
4、还原数据库状态
服务器载入RDB护着AOF文件并根据文件记录的内容还原服务器的数据库状态;
5、执行事件循环
开始执行服务器事件循环(loop)至此服务器初始化工作完成。