第二部分 单机数据库的实现
数据库
服务器中的数据库
- Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量使用redisServer.dbnum属性保存
切换数据库
- 客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库
数据库键空间
- 数据库主要由dict和expires两个字典域构成,其中dict字典负责保存键值对,而expires字典则负责键的过期时间
- 因为数据库由字典构成,因此对数据库的操作都是建立在对字典操作之上
- 数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象、有序集合对象。
设置键的过期时间
- expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间以毫秒为单位的UNIX时间戳
过期键删除策略
-
三种不同的删除策略
- 定时删除
- 在设置一个键的同时,创建一个定时器,让定时器在键过期时间来临时,立即执行对键的删除操作
- 优点: 对内存友好,能尽快地将过期键占用的内存释放
- 缺点: 对时间不友好,如果过期键很多,那么会占用大量CPU时间,影响服务器响应时间和吞吐量
- 惰性删除
- 放任过期键不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期,就删除该键;如果没有过期,就返回该键
- 优点:对时间友好,只有当取出过期键时,才将该键删除
- 缺点:对空间不友好,大量过期无用键占用内存,由内存泄露的风险
- 定期删除
- 每个一段时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,以及要检查多少个数据库,由算法决定
- 优点: 对上面两种策略的折衷。 对内存友好,对空间友好
- 关键是如何决定删除操作执行的时常和频率
- 定时删除
-
Redis的过期键删除策略
-
Redis使用的是
定期删除
+惰性删除
保证过期键一定能被删除。并合理利用CPU时间和避免内存空间浪费 -
惰性删除 : 在执行命令之前,对输入的键进行过期检查
-
定期删除 : 在规定时间内,分多次遍历服务器中多个数据库,从数据库中的expires字典随机检查一部分键的过期时间,并删除其中的过期键。
-
AOF、RDB和复制功能对过期键的处理
- 执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已过期的键
- 执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已过期的键
- 当一个过期键被删除之后,服务器会追加一条DEL命令到现有的AOF文件末尾,显示地删除过期键
- 当载入RDB or AOF文件时,会对文件保存的键进行检查,过期的键会被忽略。
- 从服务器即使发现过期键也不会主动删除,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器的数据一致性。
数据库通知
- 当Redis命令对数据库进行修改之后 , 服务器会根据配置向客户端发出数据库通知 (PUB/SUB)
- 键空间通知: 某个键执行了什么命令(SET / EXPIRE / DEL)
- 键事件通知: 某个命令被哪些键执行了 (KEY1 / KEY2 / KEY3)
RDB持久化
前置知识: 进程和子进程
可以看出,子进程和父进程的代码区是共享的,而数据区和PCB块是父进程的副本。
子PCB中的PID字段为新分配子进程PID,数据集字段为数据集地址。
父进程和子进程是可以并行执行的。互不干扰。
RDB文件的创建与载入
-
RDB持久化通过保存数据库中的键值对来记录数据库的状态 , 生成经过压缩的二进制文件。
-
创建过程
- SAVE命令由服务器进程直接执行保存操作,因此该命令会阻塞服务器
- BGSAVE由子进程执行保存操作,所以该命令不会阻塞服务器
-
载入过程
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件还原数据库状态
- 如果AOF处于关闭状态,服务器才会使用RDB文件来还原数据库状态(前者丢失的数据更少)
自动间隔性保存
-
服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。
#redis.conf 格式: save 时间 修改次数 save 900 1 (900s内修改1次) save 300 10 save 60 10000 (60s内修改10000次)
struct redisServer{ struct saveparam *saveparams; //记录保存条件的数据 long long dirty; //修改计数器 time_t lastsave; //上一次执行保存的时间 }
RDB文件的结构
- 对于不同类型的键值对,RDB文件会使用不同的方式来保存他们
AOF持久化
AOF(Append Only File)持久化实现
-
RDB持久化通过保存数据库中的键值对来记录数据库状态的不同
-
AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
-
AOF文件中所有命令都是以Redis命令请求协议的格式(文本协议)保存的
-
命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件中
- 由于内存和磁盘的输入/输出速度不匹配,因此会将数据先写入缓冲区。系统提供了
fsync, fdatasync
两个同步函数(系统调用),让操作系统立即将缓冲区的数据写入硬盘中,减少缓冲区由于宕机而丢失数据的影响
- 由于内存和磁盘的输入/输出速度不匹配,因此会将数据先写入缓冲区。系统提供了
-
appendfsync选项的不通值对AOF持久化功能的安全性和Redis服务器的性能有很大的影响
- always : 每个事件循环都将aof_buf缓冲区内容写入同步到AOF文件
- everysec(默认) : 每个事件循环后,判断上一次AOF是否间隔1S,如果是,则将aof_buf缓冲区内容写入同步到AOF文件。 因此就算故障停机,缓存也只丢失1S的数据。
- no : 什么时候将缓冲区内容同步到AOF文件中,由操作系统决定
AOF文件的载入与数据还原
- 服务器只要载入并重新执行保存在AOF文件中的命令(使用伪客户端),就可以还原数据库本来的状态了。
AOF重写
- 为了解决AOF体积膨胀的问题,提供了AOF重写机制。AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件保存的数据库状态是一样的,但体积更小
- AOF重写是一个由歧义的名字,程序无需对现有AOF文件进行任何装入、分析和写入操作。它是通过读取数据库中的键值对来实现的。
- AOF重写程序放在子进程中执行,此时服务器进程可以继续处理命令请求
- 子进程带有服务器进程数据的副本(数据一致性问题),那么如果在重写过程中有新的写请求更改数据库状态,就会产生当前数据库状态与重写后的AOF文件状态不一致问题。
- 在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作后,服务器会将重写缓冲区中的所有内容追加到新的AOF文件的末尾,使得新旧两个AOF文件所保存的数据状态一致。随后,用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
- 在AOF重写期间,服务器的执行工作
- 执行客户端的命令
- 将执行后的写命令追加到AOF缓冲区(保证旧的AOF文件完整)
- 将执行后的写命令追加到AOF重写缓冲区(用于解决数据不一致问题)
- 在AOF重写期间,服务器的执行工作
事件
Redis服务器是一个
事件驱动程序
,服务器处理的事件分为文件事件和时间事件两类
文件事件
- 文件事件处理器是基于
Reactor模式
实现的网络通信程序 - 文件事件处理器使用
IO多路复用
程序来同时监听多个套接字。并根据套接字目前执行的任务来为套接字关联不同的事件处理器
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)时,与操作对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
- 文件事件是对套接字操作的抽象,每次套接字变为可应答(acceptable)、可写(writeable)或者可读(reable)时,相应的文件事件就会产生
- 文件事件分为AE_READABLE事件(读事件)和AE_WRITEABLE事件(写事件)两类
-
一次完整的客户端与服务端连接事件示例
-
Redis服务器运行时, 将连接应答处理器与 AE_READABLE事件关联起来
-
当Redis客户端发起连接时,那么监听套接字将产生AE_READABLE事件,触发连接应答处理器执行。处理跟客户端建立连接,并将客户端套接字的AE_READABLE事件与命令请求处理器关联起来
-
当客户端向redis发起请求的时候,那么客户端套接字将产生AE_READABLE事件,然后由对应的命令请求处理器来处理。读取客户端的命令内容,并传给相应程序执行。
-
那么当redis准备好给客户端响应数据之后,服务端会将AE_WRITEABLE事件跟命令回复处理器关联起来。当客户端准备尝试读取响应数据时,客户端套接字就会产生AE_WRITEABLE事件,触发命令回复处理器执行处理,将准备好的数据返回给客户端。 当回复写完时,服务器就会解除客户端套接字的AE_WRITABLE事件与命令回复处理器之间的关联。
-
时间事件
-
时间事件分为定时事件和周期性事件;定时事件只在指定时间到达一次,而周期性事件则每隔一段事件到达一次。
-
服务器在一般情况下只执行serverCorn函数一个时间事件,并且是周期性的(100ms一次)
事件实现的三个属性: id:时间事件全局ID , when:事件到达时间 timeProc:事件处理函数 与一个由事件节点构成的无序链表
事件的调度与执行
-
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件过程中不会发生抢占。
-
时间事件的实际处理事件通常会比设定的到达晚一些(因为无法中断文件事件)
-
ServerCron是Redis周期性事件的主要函数。 它的工作主要包括
- 更新服务器的各类统计信息,如时间,内存占用
- 清理数据库过期键值对
- 尝试进行AOF和RDB操作等等
客户端
-
服务器状态结构使用clients链表表示连接了多少个客户端状态,新添加的客户端状态会被放到链尾
-
客户端状态flags属性使用不同标志来表示客户端的角色,以及客户端当前所在状态
-
输入缓冲区记录了客户端发送的命令请求,这个缓冲区大小不超过1GB
-
客户端使用argv , argc两个属性记录命令的参数和个数 , 而cmd属性记录了客户端要执行命令的实现函数
-
客户端有固定大小缓冲区和可变大小缓冲区两种, 其中固定大小缓冲区最大大小为16KB , 而可变大小缓冲区(由多个缓冲区组成, 用链表链接)最大大小不能超过服务器设置的硬性限制值
-
输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭 ; 除此之外 ; 如果客户端在一定时间内,一直超过服务器设置的软性限制,那么客户端也会关闭.
#设置硬性 , 软性链接 命令名 客户端角色 硬性链接 软性链接 软性链接时长 client-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60
-
客户端关闭的原因 : 网络连接关闭 ; 发送了不合格时的命令请求 ; 成为CLIENT KILL目标 ; 空转时间超时 ; 输出缓冲区的大小超出限制.
服务端
- 一个命令请求从发送到完成要经历的步骤:
- 客户端将命令请求发给服务器
- 服务器读取命令请求,并分析命令参数
- 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复
- 执行预备操作: 如检验命令的格式 ; 内存是否足够 ; 命令此时是否合法 ; 查看是否开启事务
- 调用命令实现函数
- 执行后续操作: 更改统计信息,如耗费时长 ; 如果开启了AOF还要往缓冲区写数据 ; 如果它是master,那么还要将数据同步到从服务器
- 服务器将命令回复返回给客户端
- ServerCron函数(每隔100ms执行一次,维护服务器相关资源,并做统计)
- 更新服务器时间缓存
- 更新LRU时钟 (空转时间 = LRU时钟 - 某个键上次访问时间 )
- 更新服务器每秒执行的命令数 (统计吞吐量) ; 更新内存峰值
- 处理SIGTERM信号(中断信号)
- 管理数据库资源(检查过期键)
- 将AOF缓冲区内容写入AOF (每次事件循环时都会做出检查)
- 服务器从启动到能够处理客户端请求经过的步骤
- 初始化服务器状态
- 载入服务器配置
- 初始化服务器数据结构
- 还原数据库状态
- 执行事件循环