Redis设计与实现(第二部分):单机数据库的实现
8. 数据库
8.1 服务器中的数据库
Redis服务器将数据库保存在 redisServer 结构的db数组中,db数组的每个项都是一个 redisDb 结构,每个redisDb结构代表一个数据库,redisServer中有一个 dbnum 属性,它决定服务器初始化时创建多少个数据库,默认值为16.
8.2 切换数据库
Redis客户端默认目标数据库为0号数据库,可以通过 select 命令切换,redisClient结构的db属性 记录了客户端当前的目标数据库
8.3 数据库键空间
redisDb结构的dict字典 保存了数据库中的所有键值对,我们将这个字典称为键空间
键空间的键就是数据库的键,是一个字符串
键空间的值也就是数据库的值,可以是Redis任意一种对象
键空间是一个字典,所以针对数据库的操作,实际上都是在操作这个字典
添加新键:给字典添加新的键值对
删除键:删除对应的键值对对象
更新键:更新键所对应的值
对键取值:取出键对应的值
注意:当使用Redis命令对数据库进行读写时,服务器不仅执行读写操作,还会执行额外的维护操作,如更新命中次数,更新LRU等。
8.4 设置键的生存时间或过期时间
通过 EXPIRE 命令或者 PEXPIRE 命令,可以为数据库中的某个键设置生存时间,到期这个键就会被自动删除,TTL 和 PTTL 命令可返回这个键的剩余生存时间。
Redis有四个命令设置生存/过期时间,但最终都会转换成 PEXPIREAT 执行。
redisDb结构的 expires字典 保存了数据库中所有键的过期时间,这个过期字典为一个指针,键指向某个键对象,值为一个long long 整数,保存过期时间 。
检查键是否过期:检查该键是否存在过期字典 --> 存在获取过期时间 --> 和当前时间比较。
8.5 过期键删除策略
三种:定时删除、惰性删除、定期删除
定时删除
设置过期时间的同时,创建一个定时器,到期立即删除
对内存友好,会尽快释放内存,但是对CPU时间不友好,在内存不紧张CPU时间紧张时,影响服务器相应时间和吞吐量。
并且创建定时器需要用到时间事件,时间事件使用无序链表实现,查找事件的时间复杂度为O(N),创建大量定时器效率低。
惰性删除
不刻意去管,但每次获取键时都检查是否过期,过期了就删除
对CPU时间友好,但是对内存不友好,过期的键只要不访问就永远不会删除,这样的键多了的话会造成内存泄露。
定期删除
每隔一段时间,就检查删除一次,删除多少由算法决定
对上边两种策略的折中,但难点在于确定删除操作执行的时长和频率。
8.6 Redis的过期键删除策略
过期键的惰性删除策略由 expireIfNeeded 函数实现,expireIfNeeded函数就像一个过滤器,所以读写数据的命令执行之前都会调用它,过期就删除键返回空。
过期键的定期删除策略由 activeExpireCycle 函数实现
这个函数每次运行,都会随机取出一定数量的键检查,并删除过期的键,然后由全局变量current_db记录进度,所有数据库都被检查一遍后current_db置0,重新开始。
8.7 AOF、RDB和复制功能对过期键的处理
8.7.1 生成和载入RDB文件
生成
创建新RDB文件时,程序会对数据库中的键进行检查,过期键不会被保存到RDB文件
所以,数据库中包含过期键不会对生成新的RDB文件造成影响。
载入
如果以主服务器模式运行,载入时会对文件中保存的键进行检查并忽略过期键,所以不会造成影响。
如果以从服务器模式运行,所以的键都会被载入,但是主从同步时,从数据库会被清空,所以也不影响。
8.7.2 AOF文件写入和重写
写入
过期键不会对AOF文件产生任何影响,当过期键被删除后,程序会向AOF文件追加一条DEL命令,来显式地记录该键已被删除。
重写
和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
8.7.3 复制
从服务器的过期键删除动作统一由主服务器控制,主服务器删除过期键后,显式的向所有从服务器发送DEL命令告知删除
这样可以保证主从数据库的一致性
9. RDB持久化
Redis将自己的数据库 状态(非空数据库的键值对) 存储在内存中,这样一旦服务器退出,数据就会消失,所以为了避免数据意外丢失,就需要将数据持久化到磁盘中。
9.1 RDB文件的创建与载入
生成RDB文件的两个命令:SAVE、BGSAVE
SAVE:阻塞Redis服务器进程,直到RDB文件创建完毕为止
BGSAVE:派生出一个子进程,由子进程负责创建RDB文件,服务器进程继续处理命令请求
RDB文件的载入工作是在服务器启动时自动执行的
注:如果开启了AOF持久化,则优先使用AOF还原数据库状态,AOF关闭时,才使用RDB。
载入RDB文件期间,服务器会处于阻塞状态,直到载入完成。
9.2 自动间隔性保存
Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令
可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令,如:
save 900 1
save 300 10
save 60 10000
9.2.1 设置保存条件
Redis启动时,可以指定配置文件或者传入启动参数设置save选项,不设置就使用默认条件,然后会将这些条件保存到 redisServer结构的saveparams属性
9.2.2 dirty计数器和lastsave属性
dirty计数器:记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改
lastsave:记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间
9.3 RDB文件结构
10. AOF持久化
与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
被写入AOF文件的所有命令都是以Redis的 命令请求协议格式 (纯文本格式)保存的
10.1 AOF持久化的实现
三步:为命令追加、文件写入、文件同步
命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
文件写入与同步
Redis的服务器进程就是一个事件循环
循环中文件事件负责接收客户端的命令请求,以及向客户端发送命令回复
循环中而时间事件负责定时运行的一些函数
处理文件事件时执行的写命令,会被追加到aof_buf缓冲区
服务器每次结束一个事件循环之前,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面
10.2 AOF文件的载入与数据还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态,如下
10.3 AOF重写
随着时间的推移,AOF文件中的内容会越来越多,体积越来越大,还原所需要的时间也就越多,并且会对服务器造成不好的影响,为了解决AOF文件膨胀问题,Redis提供了AOF文件重写功能
10.3.1 AOF文件重写的实现
AOF文件重写是通过读取服务器当前的数据库状态来实现的,因此并不需要对现有的AOF文件进行读取等操作。
实现
从数据库中读取键现在的值
用一条命令去记录键值对,代替之前记录这个键值对的多条命令
从而减少命令的冗余,减小文件体积
10.3.2 AOF后台重写
因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用 aof_rewrite 函数的话,那么在重写AOF文件期间,服务期将无法处理客户端发来的命令请求,所以,Redis决定将AOF重写程序放到子进程里执行,但子进程在进行AOF重写期间,可能会对现有的数据库状态进行修改,导致数据库状态不一致问题。
解决
Redis服务器设置了一个AOF重写缓冲区,创建子进程后开始使用
Redis执行一个写命令,同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区
当子进程完成AOF重写工作之后,发信号给父进程
父进程将AOF重写缓冲区中的所有内容写入到新AOF文件
对新的AOF文件进行改名,覆盖现有AOF文件
11. 事件
11.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:文件事件处理器
文件事件处理器使用 I/O多路复用程序同时监听多个套接字,并根据套接字的当前任务关联不同的事件处理器
当被监听的套接字准备执行操作时,就会产生相应的文件,然后文件处理器就会调用关联的事件处理器处理事件
文件事件处理器的构成
四个部分:套接字、I/O多路复用程序、文件事件分派器,以及事件处理器
文件事件是对套接字操作的抽象
I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字
多个文件事件会并发的出现,I/O多路复用程序会将所以产生事件的套接字放到一个对列里边,并有序、同步的传给分派器
事件分派器根据套接字的事件类型,调用对应事件处理器
客户端与服务器连接
11.2 时间事件
Redis时间事件分为 定时事件 和 周期性事件
定时事件:让某段程序一段时间后执行一次
周期性事件:让某段程序每隔一段时间就执行一次
一个时间事件主要由以下三个属性组成:
id:全局唯一ID
when:记录事件到达时间的时间戳
timeProc:时间事件处理器,一个函数
实现
服务器将所有时间事件都放在一个无序链表中
每当时间事件执行器运行时,它就遍历整个链表
查找所有已到达的时间事件,并调用相应的事件处理器
新的时间事件总是插入到链表的表头,该链表不按when属性的大小排序,所以要确保已达事件可以全部处理到,必须遍历所有事件
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占
12. 客户端
一个Redis服务器可以与多个客户端建立网络连接,虽然Redis使用单线程单进程的方式来处理命令请求,但是通过使用由I/O多路复用技术实现的文件事件处理器,使其能够与多个客户端进行网络通信。
每个与服务器连接的客户端,服务器都会为这其建立对应的 redisClient 结构,保存客户端状态信息。
Redis服务器结构里边有一个属性 clients ,是一个链表,保存所有与服务器连接的客户端。
12.1 客户端属性
看注释
typedef struct redisClient {
// 套接字描述符
int fd;
// 当前正在使用的数据库
redisDb *db;
// 当前正在使用的数据库的 id (号码)
int dictid;
// 客户端的名字
robj *name;
// 输入缓冲区:保存客户端发送的命令请求
sds querybuf;
// 查询缓冲区长度峰值
size_t querybuf_peak;
// querybuf中命令参数数量
int argc;
// querybuf中命令参数对象数组
robj **argv;
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
// 请求的类型:内联命令还是多条命令
int reqtype;
// 剩余未读取的命令内容数量
int multibulklen;
// 命令内容的长度
long bulklen;
// 回复链表
list *reply;
// 回复链表中对象的总大小
unsigned long reply_bytes;
// 已发送字节,处理 short write 用
int sentlen;
// 创建客户端的时间
time_t ctime;
// 客户端最后一次和服务器互动的时间
time_t lastinteraction;
// 客户端的输出缓冲区超过软性限制的时间
time_t obuf_soft_limit_reached_time;
// 客户端状态标志
int flags;
// 当 server.requirepass 不为 NULL 时
// 代表认证的状态
// 0 代表未认证, 1 代表已认证
int authenticated;
// 复制状态
int replstate;
// 用于保存主服务器传来的 RDB 文件的文件描述符
int repldbfd;
// 读取主服务器传来的 RDB 文件的偏移量
off_t repldboff;
// 主服务器传来的 RDB 文件的大小
off_t repldbsize;
sds replpreamble;
// 主服务器的复制偏移量
long long reploff;
// 从服务器最后一次发送 REPLCONF ACK 时的偏移量
long long repl_ack_off;
// 从服务器最后一次发送 REPLCONF ACK 的时间
long long repl_ack_time;
// 主服务器的 master run ID
// 保存在客户端,用于执行部分重同步
char replrunid[REDIS_RUN_ID_SIZE+1];
// 从服务器的监听端口号
int slave_listening_port;
// 事务状态
multiState mstate;
// 阻塞类型
int btype;
// 阻塞状态
blockingState bpop;
// 最后被写入的全局复制偏移量
long long woff;
// 被监视的键
list *watched_keys;
// 这个字典记录了客户端所有订阅的频道
// 键为频道名字,值为 NULL
// 也即是,一个频道的集合
dict *pubsub_channels;
// 链表,包含多个 pubsubPattern 结构
// 记录了所有订阅频道的客户端的信息
// 新 pubsubPattern 结构总是被添加到表尾
list *pubsub_patterns;
sds peerid;
// 回复偏移量
int bufpos;
// 回复缓冲区
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
12.2 客户端的创建与关闭
如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
客户端被关闭的原因:网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制等。
处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。
13. 服务器
13.1 命令请求的执行过程
以 SET KEY VALUE
1)客户端向服务器发送命令请求SET KEY VALUE。
客户端将请求转换成协议格式,通过连接到服务器的套接字,将请求发送给服务器
2)服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK。
读取套接字中命令请求,保存到客户端状态的输入缓冲区
分析命令,提取命令参数和个数保存
调用执行器执行
3)服务器将命令回复OK发送给客户端。
4)客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。
13.2 serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。
工作:
更新服务器时间缓存
更新LRU时钟
更新服务器每秒执行命令次数
更新服务器内存峰值记录
处理SIGTERM信号
管理客户端资源
管理数据库资源
执行被延迟的BGREWRITEAOF
检查持久化操作的运行状态
将AOF缓冲区中的内容写入AOF文件
关闭异步客户端
增加cronloops计数器的值
13.3 服务器初始化
服务器从启动到能够处理客户端的命令请求需要执行以下步骤
1)初始化服务器状态;
2)载入服务器配置;
3)初始化服务器数据结构;
4)还原数据库状态;
5)执行事件循环。
OVER(∩_∩)O~