Redis单机数据库的实现
数据库
服务器中的数据库
Redis服务器的所有数据库都保存在服务器状态redis.h/redisServer结构的db数组内,db数组的每个项都是一个redis.h/redisDb结构。
struct redisServer {
// ...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
// 服务器的数据库数量,默认为16个
int dbnum;
// ...
}
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库:
typedef struct redisClient {
// ...
// 记录客户端当前正在使用的数据库
redisDb *db;
// ...
} redisClient;
如下图,某个客户端的目标数据库是1号数据库:
通过修改redisClint.db指针,让它指向服务器中不同的数据库,从而实现切换目标数据库的功能。
数据库键空间
Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redisDb结构表示
typedef struct redisDb {
// ...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
// ...
} redisDb;
键空间和用户所见的数据库是直接对应的:
- 键空间的键也就是数据库中的键,每个键都是一个字符串对象。
- 键空间的值也就是数据库中的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合中的任意一种Redis对象。
下面是键空间的一个示例图:
使用Redis命令对数据库进行读写时,服务器除了对键空间执行指定的读写操作外,还会执行一些额外的维护操作,包括:
- 读取一个键时,服务器会根据这个键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数。
- 读取一个键之后,服务器会更新键的LRU时间。
- 如果服务器读取一个键时发现该键已经过期,那么服务器会先删除这个过期键。
- 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
- 服务器每次修改一个键之后,都会对脏键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作。
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器会按配置发送相应的数据库通知。
设置键的生存时间或过期时间
设置生存时间(Time To Live,TTL):
- EXPIRE <key> <ttl>:以秒为精度为某个键设置生存时间,将键key的生存时间设置为ttl秒
- PEXPIRE <key> <ttl>:以毫秒为精度为某个键设置生存时间,将键key的生存时间设置为ttl毫秒
设置过期时间(Expire Time):
- EXPIREAT <key> <timestamp>:以秒为精度为某个键设置过期时间,将键key的过期时间设置为timestamp所指定的秒数时间戳
- PEXPIREAT <key> <timestamp>:以毫秒为精度为某个键设置过期时间,将键key的过期时间设置为timestamp所指定的毫秒数时间戳
注:设置生存时间或过期时间底层都是通过PEXPIREAT命令来实现的。
redisDb结构中有一个expires字典,保存了这个数据库中所有键的过期时间:
typedef struct redisDb {
// ...
// 过期字典,保存着键的过期时间
dict *expires;
// ...
} redisDb;
下面是带有过期时间的redisDb的示例:
移除键的过期时间:通过PERSIST命令可以移除一个键的过期时间。
计算并返回剩余生存时间:
- TTL <key>:以秒为单位返回键的剩余生存时间
- PTTL <key>:以毫秒为单位返回键的剩余生存时间
过期键的判定:
- 检查给定键是否存在与过期字典expires中,如果存在,那么取得键的过期时间;
- 检查当前UNIX时间戳是否大于键的过期时间,如果是的话,那么键已经过期,否则的话,键未过期。
过期键的删除策略
如果一个键过期了,那么它应该什么时候被删除?当前主要有三种方案:
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
定时删除的优缺点:
- 优点:对内存最友好,保证键尽可能快地被删除,并释放过期键所占用的内存。
- 缺点:对CPU时间最不友好,在过期键比较多的时候,删除键这个操作会占用相当一部分的CPU时间。其次,创建一个定时器用到Redis的时间事件,查找一个事件的时间复杂为O(N),因此使用定时删除策略在现阶段来说不太现实。
惰性删除的优缺点:
- 优点:对CPU时间最友好,这个策略删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何CPU时间。
- 缺点:对内存最不友好,如果某些键已经过期,但是这些过期键恰好又没有被访问到的话,那么它们也许永远也不会被删除。
定期删除的优缺点:
- 优点:是前面两种策略的一种整合和折中。首先是限制删除操作执行的时长和频率,来减少删除操作对CPU时间的影响。其次是通过定期删除过期键,能有效地减少了因为过期键带来的内存浪费。
- 缺点:比较难确定删除操作执行的时长和频率。如果删除执行太频繁,或者执行时间太长,定期删除策略会退化为定时删除策略。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会退化成惰性删除策略。
Redis的过期键删除策略
Redis服务器实际上是使用惰性删除和定期删除两种策略。
1、惰性删除策略的实现
惰性删除策略通过db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查。
expireIfNeeded函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的输入键,从而避免命令接触到过期键。
2、定期删除策略
定期删除策略通过redis.c/activeExpireCycle函数实现,每当服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定时间内,分多次遍历服务器中的各个数据库,从数据库的过期字典expires中随机检查一部分键的过期时间,并删除其中的过期键。
activeExpireCycle函数的工作模式总结如下:
- 函数每次运行时,都从一定数量的数据库中取出一定数据的随机键进行检查,并删除其中的过期键。
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。
- 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这是函数将current_db变量重置为0,然后再次开始新一轮的检查工作。
AOF、RDB和复制功能对过期键的处理
1、RDB的处理策略
新建RDB文件:使用SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
载入RDB文件:
- 如果服务器以主服务器模式运行时,载入RDB文件时,会对文件中保存的RDB键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略。
- 如果服务器以从服务器模式运行,载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。
2、AOF的处理策略
AOF生成:当服务器以AOF持久化模式运行时,如果某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而做任何操作,只有当这个过期键被惰性删除或定时删除后,程序才会向AOF文件追加一条DEL命令,来显式记录该键已被删除。
AOF重写:在执行AOF重写时,程序会对数据库中的键进行检查,已过期的键不会被保存重写后的AOF文件中。
AOF在主从复制模式下,服务器的过期键删除动作由主服务器控制(保证主从服务器数据的一致性):
- 主服务器在删除一个过期键之后,会显式向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的命令时,即使碰到过期键也不会将过期键删除,而是继续处理未过期的键一样来处理过期键。
- 从服务器只有在接到主服务器发送的DEL命令之后,才会删除过期键。
数据库通知
数据库通知允许客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
- 键空间通知:关注某个键执行了什么命令
- 键事件通知:关注某个命令被什么键执行了
RDB持久化
持久化:由于Redis是一个内存数据库,数据都保存在内存中,一旦服务器进程退出,或者内存断电,那么数据就会丢失,因此Redis提供了两种持久化手段(RDB和AOF),支持将数据保存到磁盘中。
RDB:Redis DataBase
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。
RDB文件的生成和载入
1、RDB文件的生成
Redis有两个命令可以生成RDB文件:
- SAVE:SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕,在服务器阻塞期间,服务器不能接受任何命令请求。
- BGSAVE:BGSAVE命令会fork一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理其他命令请求。
需要注意:
服务器在执行BGSAVE命令期间,Redis虽然可以继续处理客户端的命令请求,但是会拒绝客户端发送的SAVE命令和BGSAVE命令。
BGSAVE命令不能和BGREWRITEAOF命令同时执行:
- 如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
- 如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
2、RDB文件载入
RDB文件的载入工作是在服务器启动时自动执行的,所有Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件的存在,就会自动载入这个RDB文件。
需要注意,由于AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
自动间隔性保存
Redis服务器允许用户配置save选项,让服务器每隔一段时间(满足其中一项配置选项时触发)自动执行一个BGSAVE命令。默认的save配置如下:
save 900 1 # 900秒内执行过至少1次修改
save 300 10 # 300秒内执行过至少10次修改
save 60 10000 # 60秒内执行过至少10000次修改
服务器在redisServer结构的saveparams属性中保存这个配置:
struct redisServer {
// ...
// 记录了保存条件的数组
struct saveparam *saveparams;
// ...
}
struct saveparam {
// 秒数
time_t seconds;
// 修改数
int changes;
}
除了saveparams数组之外,服务器状态还维持一个dirty计数器,以及一个lastsave属性:
- dirty计数器:记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、删除等操作)。
- lastsave属性:该属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
struct redisServer {
// ...
// dirty计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
// ...
}
如何检查保存条件是否满足?
Redis服务器会周期性(默认100毫秒一次)执行serverCron函数,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
RDB文件结构
- REDIS:长度为5个字节,保存’R’、‘E’、‘D’、‘I’、'S’这5个字符,用于程序在载入文件时,快速检测所载入的文件是否为RDB文件。
- db_version:长度为4个字节,表示RDB文件的版本号,比如"0006"就代表这个RDB文件的版本为第六版。
- databases:包含零个或任意多个数据库,以及各个数据库中的键值对数据。
- EOF:1字节,标志着RDB文件正文内容的结束,当读入程序遇到这个值时,它知道所有数据库的所有键值对都已经载入完毕了。
- check_sum:8字节长的无符号整数,保存着一个校验和。这个校验和是程序通过对前面四部分的内容进行计算得出的。服务器在载入RDB文件,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
AOF持久化
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF:Append Only File
AOF持久化的实现
1、命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾;
struct redisServer {
// ...
// AOF缓冲区
sds aof_buf;
// ...
}
2、AOF文件的写入与同步
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
在服务器处理文件事件时,可能有一些写命令在这个期间被执行,使得一些内容被追加到aof_buf缓冲区里面,所以服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里,伪代码如下:
def eventLoop():
while True:
# 处理文件事件,接受命令请求以及发送命令回复
# 处理命令请求时可能会有新内容被追加到aof_buf缓冲区中
processFileEvents()
#处理时间事件
processTimeEvents()
#考虑是否要将aof_buf中的内容写入和保存到AOF文件中
flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定:
- always:将aof_buf缓冲区中的所有内容写入并同步到AOF文件
- everysec:将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,那么在此对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的。
- no:将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统决定。
appendfsync选项的默认值是everysec。
文件的写入和同步
为了提高文件的写入效率,在现代操作系统中,当用户调用write函数将一些数据写入到文件的时候,操作系统会暂时将写入数据暂时保存在一个内存缓冲区里,等缓冲区的空间被填满时、或者超过指定时限后,才真正将缓冲区的数据写入到磁盘里。
这种做法虽然提高了写入效率,但是如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失。因此系统提供了fsync和fdatasync两个同步函数。
三种模式的特点:
- always:效率最慢,但是最安全,当出现故障停机也只会丢失一个事件循环所产生的命令数据。
- everysec:效率足够快,即使出现故障停机,也只是丢失一秒钟的命令数据。
- no:该模式下AOF文件的写入速度是最快的,但是这种模式会在系统缓存中积累一段时间的写入数据,所以该模式的单次同步时长通常是三种模式中时间最长的。当出现故障停机时,会丢失上次同步AOF文件之后的所有写命令数据。
AOF文件的载入与数据还原
因为AOF文件保存的是重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原数据库原来的状态。
具体详细步骤如下:
- 创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文执行,而载入AOF文件时所使用的命令直接源自于AOF文件而不是网络连接,所以服务使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令。
- 从AOF文件中分析并读取出一条写命令。
- 使用伪客户端执行被读出的写命令。
- 一直执行步骤2和步骤3,直到AOF文件所保存的数据库状态被完整地还原。
AOF重写
因为AOF持久化是通过保存被执行的写命令来记录数据库状态,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积越来越大,如果不加处理,可能会对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件越大,进行数据还原花费的时间越长。
Redis提供AOF文件重写(rewrite)功能,通过创建一个新的AOF文件代替现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新的AOF文件只包含还原当前数据库状态所必须的命令,不会有任何浪费空间的冗余命令。
1、AOF重写实现原理
遍历所有数据库的所有键,将生成这个键值对的命令写入到新的AOF中,伪代码如下:
def aof_rewrite(new_aof_file_name):
# 创建新的AOF文件
f = create_file(new_aof_file_name)
# 遍历数据库
for db in redisServer.db:
# 忽略空的数据库
if db.is_empty(): continue
# 写入SELECT命令,指定数据库号码
f.write_command("SELECT" + db.id)
# 遍历数据库中的所有键
for key in db:
# 忽略已经过期的键
if key.is_expired(): continue
# 根据键的类型对键进行重写
if key.type == String:
rewrite_string(key)
elif key.type == List:
rewrite_list(key)
elif key.type == Hash:
rewrite_hash(key)
elif key.type == Set:
rewrite_set(key)
elif key.type == SortedSet:
rewrite_sorted_set(key)
# 如果键带有过期,也需要重写过期时间
if key.have_expire_time():
rewrite_expire_time(key)
# 写入完毕,关闭文件
f.close()
2、AOF后台重写(BGREWRITEAOF)
由于重写函数会进行大量的IO操作,会造成调用这个函数的线程被长时间阻塞,如果Redis服务器直接调用aof_rewrite函数,那么在重写AOF文件期间,服务器将无法处理来自客户端的命令请求。
因此Redis是通过fork一个子进程去完成AOF重写,这样做有两点优势:
- 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
这样做也有一个问题:在子进程进行AOF重写期间,父进程仍在接受客户端的写命令,会出现重写AOF文件与真正数据库状态不一致。
为了解决这种数据状态不一致问题,Redis设置一个AOF重写缓冲区,这个缓冲区在创建子进程进行重写之后开始使用。在重写期间,当Redis服务执行完一个写命令之后,它不仅将写命令发送给AOF缓冲区,也会将写命令发送给AOF重写缓冲区。
在子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,这时新的AOF文件所保存的数据库状态与服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件替换。
注意:只有信号处理函数执行时会对服务器进程(父进程)造成阻塞。
事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
- 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他Redis服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些文件事件来完成一系列网络通信操作。
- 时间事件:Redis服务器的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
文件事件
Redis基于Reactor模式设计了文件事件处理器:
- 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
1、文件事件处理器的构成(四个组成部分):
- 套接字:文件事件是对套接字操作的抽象,这些操作包括accept、read、write、close。
- I/O多路复用程序:负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。
- 文件事件分派器(dispatcher):接受I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。
- 事件处理器:事件处理器就是一个函数,定义了某个事件发生时,服务器应该执行的动作。
尽管多个文件事件可能并发出现,但I/O多路复用程序会将所有产生事件的套接字放到一个队列里,通过这个队列可以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。
2、I/O多路复用程序的实现
Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。
Redis在I/O多路复用程序的实现源码中使用宏定义了相应的规则,程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现。
evport > epoll > kqueue > select
3、事件的类型
I/O多路复用程序可以监听多个套接字的AE_READABLE事件和AE_WRITABLE事件,这两类事件和套接字之间的对应关系如下:
- 套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
- 套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。
注意:如果一个套接字同时产生了这两种事件,文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完毕之后,才处理AE_WRITABLE事件。即优先处理读套接字,后处理写套接字。
4、事件API
aeCreateFileEvent函数:接受一个套接字描述符、一种事件类型,以及一个事件处理器作为参数,将给定套接字的给定事件加入到I/O多路复用程序的监听范围内,并对事件和事件处理器进行关联。
aeDeleteFileEvent函数:接受一个套接字描述符和一个监听事件类型作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联。
aeGetFileEvent函数:接受一个套接字描述符,返回该套接字正在被监听的事件类型:
- 如果套接字没有任何事件被监听,那么函数返回AE_NONE;
- 如果套接字的读事件正在被监听,那么函数返回AE_READABLE;
- 如果套接字的写事件正在被监听,那么函数返回AE_WRITEABLE;
- 如果套接字的读事件和写事件正在被监听,那么函数返回AE_READABLE|AE_WRITEABLE;
aeWait函数:接受一个套接字描述符、一个事件类型和一个毫秒数为参数,在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回。
aeApiPoll函数:接受一个sys/time.h/struct timeval结构为参数,并在指定的时间内,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生文件事件,当至少一个事件产生,或者等待超时后,函数返回。
aeProcessEvents函数:文件事件分派器,它先调用aeApiPoll函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件。
aeGetApiName函数:返回I/O多路复用程序底层所使用的I/O多路复用函数库的名称,比如返回“epoll”表示底层为epoll函数库。
5、文件事件处理器
1)连接应答处理器:acceptTcpHandler函数,用于对连接服务器监听套接字的客户端进行应答。当Redis服务器进行初始化时,会将连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,当客户端请求连接时,服务器监听套接字就会产生AE_READABLE事件,引发连接应答处理器执行相应的套接字应答操作。
2)命令请求处理器:readQueryFromClient函数,负责从套接字中读入客户端发送的命令请求内容。当一个客户端成功连接到服务器后,服务器将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求时,套接字就会产生AE_READABLE事件,引发命令请求处理器执行相应的套接字读入操作。在客户端连接服务器的整个过程,服务器会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。
3)命令回复处理器:sendReplyToClient函数,负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。当服务器有命令回复要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接受服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行相应的套接字写入操作。当命令发送完毕之后,服务器会解除命令回复处理器和客户端套接字AE_WRITABLE事件的关联。
客户端和服务器的通信过程:
时间事件
Redis的时间事件分为两类:
- 定时事件:让一段程序在指定的之间之后执行一次。
- 周期性事件:让一段程序每隔指定时间就执行一次。
时间事件的三个属性:
- id:服务器为时间事件创建的全剧唯一ID,ID号从小到大顺序递增。
- when:毫秒精度的UNIX时间戳,记录时间事件的到达时间。
- timeProc:时间事件处理器,一个函数。
一个时间事件是定时事件还是周期性事件取决于时间处理器的返回值:
- 如果事件处理器返回AE_NOMORE,那么这个时间事件为定时事件。
- 如果事件处理器返回一个非AE_NOMORE的整数值,那么这个时间事件为周期性事件。
注:目前Redis只使用周期性事件,而没有使用定时事件。
1、时间事件的实现
所有的时间事件都放在一个无序链表中,每当时间事件处理器运行时,就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
无序是指时间事件链表不按when属性的大小排序。
在当前Redis版本中,正常模式下的Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件,所以无序链表几乎是退化为一个指针来使用,因此不会影响时间事件处理器的性能。
2、API
aeCreateTimeEvent函数:接受一个毫秒数milliseconds和一个时间事件处理器proc作为参数,将一个新的时间事件添加到服务器,这个新的时间事件将在当前时间的milliseconds毫秒之后到达,而事件的处理器为proc。
aeDeleteFileEvent函数:接受一个时间事件ID作为参数,然后从服务器中删除该ID所对应的时间事件。
aeSearchNearestTimer函数:返回到达时间距离当前时间最接近的那个时间事件。
processTimeEvents函数:时间事件的执行器,这个函数会遍历所有已到达的时间事件,并调用这些事件的处理器。
3、serverCron函数
serverCron函数负责定期对Redis服务器自身的资源和状态进行检查和调整,主要工作包括:
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
- 清理数据库中的过期键值对。
- 关闭和清理连接失效的客户端。
- 尝试进行AOF或RDB持久化操作。
- 如果服务器是主服务器,那么对服务器进行定期同步。
- 如果处于集群模式,对集群进行定期同步和连接测试。
事件的调度和执行
由于服务器同时存在文件事件和时间事件,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时应该处理时间事件,以及花多少时间去处理它们等等。
事件的调度是由aeProcessEvents函数完成的。执行逻辑如下:
1)获取到达时间离当前时间最接近的时间事件。
2)计算最接近的时间事件距离到达还有多少毫秒,用remaind_ms表示,如果事件已经到达,将这个remaind_ms设置为0。
3)调用aeApiPoll函数,设置阻塞时间为remaind_ms。
4)处理所有已产生的文件事件。
5)处理所有已到达的时间事件。
6)循环步骤1- 步骤5。
执行逻辑解释:
- aeApiPoll函数的最大阻塞事件是由到达时间最接近当前时间的时间事件决定的,这样做可以避免服务器频繁对时间事件进行轮询。
- 对文件事件和时间事件的处理都是同步、有序、原子地执行,服务器不会中途中断事件处理,也不会对事件进行抢占。如果某个事件阻塞时间过长,会在需要时主动让出执行权,从而减低事件饥饿的可能性。