Redis读书笔记(三)单机数据库的实现
单机数据库的实现
1、数据库
Redis 服务器的所有数据库都保存在 redisServer.db 数组中,而数据库的数量则由 redisServer.dbnum 属性保存。
Redis 服务器默认会创建16个数据库。
每个 Redis 客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读命令的时候,目标数据库就会成为这些命令的操作对象。默认情况下,Redis 客户端的目标数据库为0号数据库,但客户端可以通过执行 SELECT 命令来切换目标数据库。
在服务器内部,客户端状态 redisClient 结构的 db 属性记录了客户端当前的目标数据库,这个属性是一个指向 redisDb 结构的指针。
数据库主要由 dict 和 expires 两个字典构成,其中 dict 字典负责保存键值对,而 expires 字典则负责保存键的过期时间。
typedef struct redisDb{ // ... // 数据库键空间,保存着数据库中的所有键值对 dict *dict; // 过期字典,保存着键的过期时间 dict *expires; // ... }
因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
数据库的键总是一个字符串对象,而值则可以是任意一种 Redis 对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象。
expires 字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的 UNIX 时间戳。
设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
- EXPIRE <key> <ttl> 命令用于将键 key 的生存时间设置为 ttl 秒。</ttl></key>
- PEXPIRE <key> <ttl> 命令用于将键 key 的生存时间设置为 ttl 毫秒。</ttl></key>
- EXPIREAT <key> <timestamp> 命令用于将键 key 的过期时间设置为 timestamp 所指定的秒数时间戳。</timestamp></key>
- PEXPIREAT <key><timestamp> 命令用于将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。</timestamp></key>
移除过期时间:PERSIST
计算并返回剩余生存时间:TTL(秒为单位),PTTL(毫秒为单位)
过期键的判定:
1)检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
2)检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
1.1 过期键的删除策略:
- 定时删除:在设置键的过期时间的同时,创建一个定时器( timer ),让定时器在键的过期时间来临时,立即执行对键的删除操作。(占用太多 CPU 时间,影响服务器的响应时间和吞吐量)。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。(浪费太多内存,有内存泄漏的危险)。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。(难点在于确定删除操作执行的时长和频率)。
1.2 AOF、RDB 和复制功能对过期键的处理
1)RDB
执行 SAVE 命令或者 BGSAVE 命令所产生的新 RDB 文件不会包含已经过期的键。
载入 RDB 文件,如果服务器以主服务器模式运行,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略;如果服务器以从服务器模式运行,那么在载入 RDB 文件时,文件中保存的所有键,无论是否过期,都会被载入到数据库中。(主从服务器在进行数据同步的时候,从服务器的数据库就会被清空)。
2)AOF
当过期键被惰性删除或定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地记录该键已被删除。
执行 BGREWRITEAOF 命令所产生的重写 AOF 文件不会包含已经过期的键。
3)复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键之后,会显式的向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- 从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。
2、RDB 持久化
RDB 持久化通过保存数据库中的键值对来记录数据库状态。
RDB 持久化功能可以将 Redis 在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB 持久化功能所生成的 RDB 文件是一个经过压缩的二进制文件,用于保存和还原 Redis 服务器所有数据库中的所有键值对数据。
有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。
SAVE 命令会阻塞 Redis服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE 命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求。
Redis服务器仍然可以继续处理客户端的命令请求,但BGSAVE 命令执行期间:
1)客户端发送过来的 SAVE 命令会被服务器拒绝。
2)客户端发送的 BGSAVE 命令会被服务器拒绝。
3)BGREWRITEAOF 和 BGSAVE 两个命令不可同时执行。
因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 redis 允许用户通过设置服务器配置的 save 选项,让服务器每个一段时间自动执行一次 BGSAVE 命令。
// save 选项默认条件,满足任意一个,BGSAVE 命令就会被执行 save 900 1 //服务器在900秒之内,对数据库进行了至少1次修改 save 300 10 save 60 1000
Redis 的服务器周期性操作函数 serverCron 默认每隔100毫秒就会执行一次,该函数用于对于正在运行的服务器进行维护,其中一项工作就是检测 save 选项所设置的保存条件是否已经满足。如果满足,则执行 BGSAVE 命令。
没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件。
如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态,只要在 AOF 持久化功能关闭时,服务器才会使用 RDB 文件来还原数据库状态。
3、AOF 持久化
AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态。
AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步三个步骤。
1)命令追加:服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf缓冲区的末尾。
struct redisServer{ // ... // AOF 缓冲区 sds aof_buf; // ... };
2)文件写入与同步:在服务器每次结束一个时间循环之前,都会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定。(always,everysync,no)
3.1 AOF 重写
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 重写功能。Redis 服务器创建一个新的 AOF 文件来代替现有的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不包含任何浪费空间的冗余命令,体积要小很多。
AOF 文件重写的实现:
AOF 文件重写不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。从数据中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。
Redis 将 AOF 重写程序放在子进程里执行,这样子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求。子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
使用子进程带来的问题:子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前数据库状态与重写后的 AOF 文件保存的数据库状态不一致。
解决方案:Redis 服务器设置了一个 AOF 重写缓冲区,该缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它同时会将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区。
1)AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作如常进行。
2)从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面。
3)当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作(会阻塞服务器进程):
- 将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新的 AOF 文件进行改名,原子地覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。
4、事件
Redis服务器是一个事件驱动程序,服务器需要处理一下两类事件:
- 文件事件:
- 时间事件
5、客户端与服务器
通过使用由 I/O 多路复用技术实现的文件事件处理器,Redis 服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
服务器为每个与服务器进行连接的客户端建立了相应的 Redis.h/RedisClient 结构(客户端状态),该结构保存了客户端当前的状态信息,以及执行相应功能时需要用到的数据结构。
struct redisServer{ // ... // 一个链表,保存了所有客户端状态 list *clients; // ... };
5.1客户端
5.1.1 客户端属性
typedef struct redisClient{ // ... int fd; // 套接字描述符 robj *name; // 名字 int flag; // 标志 sds querybuf; // 输入缓冲区 robj **argv; // argv[0]是要执行的命令,而之后的其他项则是传给命令的参数 int argc; // argv数组的长度 struct redisCommand *cmd; // 命令的实现函数 list *reply; // 可变大小缓冲区 char buf[REDIS_REPLY_CHUNK_BYTES]; // 固定大小缓冲区 int bufpos; // 记录buf数组目前已使用的字节数量 int authenticated; // 身份验证 // ... }
- 套接字描述符
- -1:伪客户端
- 大于-1的整数:普通客户端
- 名字
- 标志:记录了客户端的角色,以及客户端目前所处的状态。REDIS_MASTER 标志表示客户端代表的是一个主服务器,REDIS_SLAVE 标志表示客户端代表的是一个从服务器。
- 输入缓冲区:用于保存客户端发送的命令请求
- 命令与命令参数
- 命令的实现函数:在命令表中查找命令所对应的命令实现函数
7.输出缓冲区:用来保存执行命令所得的命令回复。1.固定大小的缓冲区;2.可变大小的缓冲区。
8.身份验证:记录客户端是否通过了身份验证
5.1.2 客户端的创建与关闭
当一个客户端通过网络连接上服务器,服务器会为这个客户端创建相应的客户端状态,并添加到 clients 链表的末尾。网络连接关闭、发送了不合协议格式的命令请求、成为 CLIENT KILL 命令的目标、空转时间超时、输出缓冲区的大小超过限制等都会造成客户端被关闭。
载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建,载入完毕后关闭。
5.2服务器
Redis 服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
5.2.1 命令请求的执行过程
1)客户端向服务器发送命令请求
2)服务器读取命令请求,并分析出命令参数
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
- 对输入缓冲区中的命令进行分析,提取命令请求中包含的命令参数,以及命令参数的个数,然后将其分别保存到客户端状态的 argv 属性和 argc 属性里面。
3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复。
- 查找命令实现。命令执行器根据客户端状态的 argv[0] 参数,在命令表中查找参数指定的命令,并将找到的命令保存到客户端状态的 cmd 属性里面。
- 执行预备操作,确保命令可以正确、顺利地被执行。
- 调用命令的实现函数,产生相应的命令回复(保存在客户端状态的输出缓冲区里面),之后还会为客户端的套接字管理命令回复处理器,负责将命令回复返回给客户端。
- 执行后续工作。
4)服务器将命令回复返回给客户端。
5.2.3 serverCron 函数
serverCron 函数默认每隔 100 毫秒执行一次,负责管理服务器的资源。
- 更新服务器时间缓存
- 更新 LRU 时钟
- 更新服务器每秒执行命令次数
- 更新服务器内存峰值记录
- 管理客户端资源
- 管理数据库资源
- 执行被延迟的 BGREWRITEAOF
- 检查持久化操作的运行状态
- 将 AOF 缓冲区中的内容写入 AOF 文件
5.2.3 初始化服务器
初始化服务器状态结构
创建一个 redisServer 类型的实例变量 server 作为服务器的状态,并为结构中的各个属性设置默认值。初始化 server 变量的工作由 initServerConfig 函数完成。
载入配置选项
用户可通过给定配置参数或者指定配置文件来修改服务器的默认配置。
初始化服务器数据结构
服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行 initServerConfig 函数时就对数据结构进行初始化,那么一旦用户通过给定配置选项修改了和数据结构有关的服务器状态属***器就有重新调整和修改已创建的数据结构。
还原数据库状态
完成了对服务器状态 server 变量的初始化之后,服务器需要载入 RDB 文件或者 AOF 文件,并根据文件记录的内容来还原服务器的数据库状态。
如果服务器启用了 AOF 持久化功能,那么服务器使用 AOF 文件来还原数据库状态,否则,使用 RDB 文件来还原数据库状态。
执行事件循环