单机数据库的实现
struct redisServer{
//服务器的数据库数量
int dbnum;
//一个数组,保存着服务器中的所有数据库
redisDb *db;
//客户端状态链表
list *clients;
//...
}
当切换库时,其实就是redisClient.db对redisServer.db数组的目标数据库指针的移动
-
数据库的键空间
-
redis将所有key进行统一管理,按照所属的库划分,放在redisDb的字典中
-
typedef struct redisDb{ //数据库键空间,保存着数据库中的所有键值对 dict *dict; ... }
-
-
键过期时间相关操作
- EXPIRE :将key的生存时间设为ttl秒。
- PEXPIRE :将key的生存时间设为ttl毫秒。
- EXPIREAT :将key的过期时间设置为timestamp秒数时间戳。
- PEXPIREAT :将key的过期时间设置为timestamp毫秒数时间戳。
-
移除过期时间
- PERSIST命令可以移除一个键的过期时间,在过期字段中查找给定键,并解除键和值在过期字典中的关联
-
计算并返回剩余生存时间
- TTL以秒为单位返回剩余时间,PTTL以毫秒返回键的剩余时间
-
过期键删除策略
- 定时删除:设置键的过期时间时,创建定时器,过期时,以定时器立刻执行键的删除。
- 惰性删除:不着急删除过期键,每次获取时都会进行过期校验。
- 定期删除:隔一段时间,程序就对数据库检查,删除过期键
-
数据库通知
- Redis发布订阅功能可以让客户端获取数据库中键的变化及命令的执行情况
- 关注某个键执行了什么命令的通知称为键空间通知—某个键执行了什么命令
- 关注某个命令被什么键执行的通知称为事件通知—某个命令被什么键执行了
- 发送通知的实现
- 通过服务器配置的值判断,如果给定通知类型不是服务器允许的就直接返回。
- 如果是服务器允许发送的,检测是否允许发送键空间通知,允许则构建发送事件并通知。
- 检测是否允许发送键事件通知,如果允许则构建并发送通知。
RDB持久化
RDS持久化(默认持久化策略)就是将某一时间点上的状态保存到一个RDB文件里。RDB文件是经过压缩的二进制文件,可通过该文件还原成数据库状态
-
SAVE会阻塞Redis服务器进程,直到RDB文件创建完毕为止,阻塞期间,服务器不能处理任何命令请求
-
BGSAVE会fork出一个子进程,由子进程负责创建RDB文件,父进程继续处理命令请求。当子进程完成之后,向父进程发送信号
- 如果服务器以主服务模式运行,程序对文件保存的键检查,未过期的载入到数据库,过期则忽略。
- 如果服务器以从服务模式运行,无论是否过期,都会载入到数据库。因为主服务器在数据同步时,会将从服务器的数据库清空,一般不会有影响。
-
BGSAVE执行期间,拒绝SAVE,BGSAVE;延迟执行BGREWRITEAOF。BGREWRITEAOF执行期间,拒绝BGSAVE
-
启动时优先加载AOF还原数据,只有在AOF处于关闭状态,才使用RDB文件恢复数据
-
自动间隔性保存
-
服务器允许用户通过配置文件设置隔一定时间自动执行BGSAVE。可通过save选项设多个保存条件
-
struct redisServer{ //记录了保存条件的数组 struct saveparam *saveparams; ... }; struct saveparam{ //秒数 time_t seconds; //修改数 int changes; };
-
struct redisServer{ //修改计数器,距离上次成功执行SAVE或BGSAVE后数据库被修改了几次 long long dirty; //上一次执行保存的时间 time_t lastsave; //... };
-
Redis的周期性操作函数serverCron每隔100毫秒会执行一次,其中一项工作就是检查save选项设置的保存条件是否满足要求,满足则执行BGSAVE
-
RDB文件结构
- REDIS:长度5字节,保存"REDIS"5个字符(为书写方便,其实是5个单独字符),通过这个判断该文件是否为RDB文件
- db_version:长度4字节,是字符串表示的整数记录RDB的版本号
- database:包含0个或多个数据库及各数据库中键值对数据。表示那些数据库是有数据的
- EOF:常量长度1字节,标志RDB文件正文的结束。读取时遇到该值,表示键值对的载入已经结束了
- check_sum:是一个8字节的无符号整数,保存一个同过前几位变量计算出来的校验和。每次加载都会进行计算校验,通过这个来判断文件是否损坏
每个非空数据库在RDB文件中都可表示为SELECTDB,db_number,key_value_pairs三部分
- selectdb:1字节,标志位,标志着下一位存储的是数据库号码
- db_number:是一个数据库号码
- key_value_pairs:保存了数据库中所有键值对数据,如果有过期时间,则过期时间也会保存
key_value_pairs部分
不带过期时间的键值对在RDB文件由TYPE,key,value组成,带过期时间则含有EXPIRETIME_MS,ms
- EXPIRETIME_MS:标志位,长度为1字节,告知程序下一个读入的是以毫秒为单位的过期时间。
- ms:是8字节长的带符号整数,记录UNIX时间戳,即过期时间。
- type:记录value的类型,长度1字节,这个常量其实就是Redis对象类型和底层编码的组装:
- 根据TYPE的不同,value的存储结构也大不相同。这里不详细展开,只需要知道,对于字符串对象,如果大于20字节,就会用LZF算法压缩。
- 除字符串对象和整数集合,其他存储方式的开头都是节点数量,告诉程序应读入多少节点/键值对
分析RDB文件
- Redis自带RDB文件检查工具redis-check-dump。可以帮助在系统故障后分析快照文件,也就是RDB文件
AOF持久化
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态。
服务器启动时,可通过载入和执行AOF文件中保存的命令来还原服务器关闭前的数据库状态。
命令追加
- 开启AOF持久化后,服务器执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾
AOF文件的写入与同步
- always:将aof_buf缓冲区的所有内容写入并同步到AOF文件。
- everysec:将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间超过一秒,就再次对AOF文件进行同步,并由一个线程专门负责。
- no:将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统决定
AOF重写
- 在新的AOF文件的重写过程中,不会读取旧AOF文件,而是通过读取数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令记录键值对,代替之前记录的多条命令
- 执行客户端发来的命令。
- 将执行后的写命令追加到AOF缓冲区
- 将执行后的写命令追加到AOF重写缓冲区
事件
Redis服务器是一个事件驱动程序
- 文件事件:Redis服务器通过套接字与客户端连接,文件事件就是服务器对套接字操作的抽象。服务器与客户端通信会产生相应文件事件,服务器通过监听这些事件来完成一系列网络通信操作
- 时间事件:Redis服务器有一些需要在给定时间内执行的操作,而时间事件就是对这类定时操作的抽象
文件事件
-
Redis基于Reactor模式开发的网络事件处理器,就是文件事件处理器
-
大致是使用I/O多路复用程序同时监听多个套接字,根据套接字目前执行的任务为套接字关联不同的事件处理器
-
当被监听的套接字准备好应答,读取,写入,关闭等操作时。与之对应的文件事件就会产生,文件事件处理器就开始发挥作用了,调用事先关联好的事件处理器来处理事件
-
多路复用程序可监听的套接字事件可分为
ae.h/AE_READABLE
事件和ae.h/AE_WRITABLE
事件- 当套接字变得可读时(客户端对套接字执行write,close,accept后),套接字产生AE_READABLE事件
- 当套接字变得可写时(客户端对套接字执行read操作后),套接字产生AE_WRITABLE事件
- 优先处理AE_READABLE,再处理AE_WRITABLE
一次完整的基于文件事件的服务器与客户端交互,相关处理器的处理过程:
- 客户端发起连接,产生读事件,触发连接应答处理器执行。创建套接字,客户端状态并将该套接字的读事件与命令请求处理器关联。
- 客户端发送命令,产生读事件,触发命令请求处理器。读取执行命令,得到回复并将该套接字的写事件与命令回复处理器关联。
- 客户端读取命令回复,产生写事件,触发命令回复处理器。将回复写入套接字,解除读事件与命令回复处理器的关联。
时间事件
-
定时事件:只在指定事件到达一次。如xx时间后执行一次。
-
周期性事件:每隔一段时间执行一次。如每隔xx秒执行一次。
-
时间事件的组成
-
id:服务器为时间事件创建的全局唯一ID (标识号)。 ID号按从小到大的顺序递增,新事件的ID号比旧事件大。
-
when:毫秒精度的UNIX时间戳,时间事件的到达(arrive)时间。
-
timeProc:时间事件处理器函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
-
-
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,遍历整个链表,找到已到达的时间事件,调用相应的事件处理器。新的事件总是插入到链表的表头。
事件的调度与执行
-
主要过程是:
-
拿到最近的时间事件并计算还有多少毫秒。
-
创建时间任务结构;阻塞等待文件时间产生,最大阻塞时间由最近时间事件到达毫秒数决定。
-
先处理已产生的文件事件再处理到达的时间事件。
-
-
执行原则/设计利弊:
-
aeApiPoll函数(redis封装的多路复用函数)的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会阻塞过长时间。
-
因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。
-
对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性
-
客户端
struct redisServer{
//一个链表,保存了所有客户端状态
list *clients
...
};
typedef struct redisClient{
//套接字描述符
int fd;
//标志
int flags;
//输入缓冲区
sds querybuf;
//单个命令拆分的数组
robj **argv;
//argv数组的长度
int argc;
//命令函数
struct redisCommand *cmd;
//固定大小输出缓冲区,默认16K
char buf[REDIS_REPLY_CHUNK_BYTES];
//buf已使用字节数
int bufpos;
//大小可变输出缓冲区
list *reply
//创建客户端的时间
time_t ctime;
//与服务器互动的最后时间
time_t lastinteraction;
//软性限制时间
time_t obuf_soft_limit_reached_time;
...
}redis client;
-
套接字描述符fd
-
根据客户端类型不同:
-
fd为-1表示伪客户端。
-
fd为大于-1的整数时表示普通客户端。
-
-
伪客户端就是用于处理的命令请求来源于AOF或Lua脚本,不需要套接字连接,也就不需要套接字记录符。普通客户端就是所有来源于网络需要套接字连接的客户端。
-
-
标志flags
- 标志flags记录了客户端的角色。有主从标志,Lua伪客户端标志,执行MONITOR标志…标志可以以二进制来拼接:flags:||…
-
输入缓冲区querybuf
- 输入缓冲区存储客户端输入的指令,大小根据输入内容动态缩小扩大,最大不可超过1G,否则导致服务器关闭该客户端。
-
命令与参数(argv,argc)
-
客户端输入的命令会先存放到数组argv中,其数据结构是这样的:
-
当客户端输入命令后,服务器根据argv[0]的值再命令表中查找(命令不区分大小写)对应命令的函数并给cmd赋值,cmd就是对应的命令函数相关的操作信息
-
-
输出缓冲区(buf,bufpos,reply)
-
输出缓冲区有两个,一个大小固定,一个大小可变。大小固定的存储长度小的回复,比如OK,错误返回等。大小可变缓冲区保存长度较大的回复,比如长列表,大集合。
-
大小可变缓冲区由reply链表实现,利用链表结构存储若干和字符串对象,使得长度不会受到限制。数据结构如下:
-
-
身份验证
- authenticated值为0,表示客户端未通过身份验证,此时发送命令会被拒绝
-
时间(ctime,lastinteraction,obuf_soft_limit_reached_time)
-
ctime属性记录创建客户端的时间,已经连接多少秒了
-
last interaction属性记录了客户端与服务器最后一次进行互动的时间,记录空转时间
-
服务器使用两种模式来限制客户端输出缓冲区的大小:
-
硬性限制( hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
-
软性限制(softlimit):软性限制比硬性限制小,服务器会根据输出缓冲区大小介于软硬性限制之间的时间决定是否关闭客户端 。
-
-
客户端的创建与关闭
-
创建普通客户端
- 客户端连接时调用connect函数,服务器就会调用连接事件处理器,为客户端创建状态,并创建新的客户端到client链表末尾。
-
关闭普通客户端
-
普通客户端可因其中一个原因关闭:
-
客户端进程退出或者被杀死
-
客户端向服务器发送了带有不符合协议格式的命令请求
-
客户端成为了CLIENT KILL命令的目标
-
用户为服务器设置了timeout配置选项且当客户端的空转时间超过timeout时。不过timeout选项有客户端是主服务器,从服务器,正在被BLPOP等命令阻塞,正在执行SUBSCRIBE、PSUBSCRIBE等订阅命令,那么即使客户端的空转时间超过了timeout选项的值,客户端也不会被服务器关闭。
-
客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1GB)
-
输出缓冲区的大小超过了硬性限制所设置的大小
-
输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制的时间超过指定时间。
-
-
-
Lua脚本的伪客户端
- 服务器初始化时创建,随服务器结束关闭。
-
AOF文件的伪客户端
- 载入AOF文件时创建,载入结束关闭。
服务器
- 客户端输入Redis指令到返回结果的执行过程
命令执行过程
-
发送
- 客户端接收命令请求时,会将命令根据协议转为固定格式再发送给服务器。
-
读取
- 当套接字因客户端的写入变得可读时,服务器会先读取协议格式内容并保存到输入缓冲区。命令分析,提取参数及个数,存入argv和argc属性。最后调用命令执行器。
-
命令执行器-查找命令的实现
-
命令表是一个字典,键是命令名字,值是redisCommand结构。几个重要属性如下:
-
name:命令名称。
-
proc:指向命令实现函数。
-
arity:命令参数个数,包括命令名称。
-
sflags:命令属性。
-
-
查找命令表的过程就是找到redisCommand,把指针指向它:
-
命令执行器-执行预备操作
- 在命令真正执行前需要有预备操作保证命令可以被正确,顺利地执行。
- 这个环节相当于一层过滤,比如检查命令是否正确,参数是否正确,身份验证是否通过,内存是否够用等等。保证配置生效,准确执行。
-
命令执行器-调用命令的实现函数
- 执行过程就是调用之前找到并指向的执行函数。通过client->cmd->proc(client);调用。
- 然后将回复保存在客户端状态的输出缓冲区中,关联该套接字的命令回复处理器。
-
命令执行器-执行后续工作
- 有一些善后工作还将继续,比如慢查询日志记录,执行时长记录,AOF持久化,主服务器将命令传给从服务器。
- 当这些都处理完后,服务器就继续从文件时间处理器中取出并执行下一个命令请求。
-
将命令回复发送给客户端
- 当客户端套接字变为可写状态,服务器执行命令回复处理器,将输出缓冲区的回复发送给客户端。
-
客户端接收并打印命令回复
- 将回复转为人类可读的格式,打印给用户看。
-
serverCron函数
-
很多情况下,Redis需要定期进行资源检查,状态同步等操作,就需要定期操作,而定期操作都是由serverCron函数负责的,也是时间事件的应用实例。默认每隔100ms执行
-
更新服务器时间缓存
-
更新LRU时钟
-
更新服务器每秒执行命令数
-
更新服务器内存峰值记录
-
处理SIGTERM信号
-
管理客户端资源
-
管理数据库资源
-
执行被延迟的BGREWRITEAOF
-
检查持久化操作的运行状态
-
将AOF缓冲区的内容写入AOF文件
-
关闭异步客户端
-
增加cronloops计数器的值
服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
-
初始化服务器状态结构
- 主要是对redisServer结构体的初始化,包括设置服务器运行ID,运行频率,设置配置文件路径,设置持久化条件,命令表创建等。
-
载入配置选项
- 根据用户设定的配置,对redisServer相关变量的值进行修改,比如端口号,数据库数量,RDB的压缩是否开启等等。其他属性还是沿用默认值。
-
初始化服务器数据结构
- 服务器必须先载入用户配置,才能对其他数据结构进行准确初始化。其他数据结构包括客户端链表,db数组,订阅信息,Lua脚本执行环境,慢查询日志相关属性等等。
-
还原数据库状态
- 载入RDB或AOF文件的数据恢复过程。
-
执行事件循环
- 至此,服务器可接收客户端请求并发送信息。