数据库
数据库结构实现
Redis的数据库都保存在服务器中,一个服务器默认有16个数据库。
Redis服务器和数据库关系如图所示,其中dbnum就是指当前数据库的数量,db是一个数据库的数组。
每个客户端都会指向一个数据库,可以通过select指令改变表当前使用的数据库。
每个数据库结构中都会有一个数据库键空间,保存着所有的键值对。
数据库结构如图所示,dict就是一个键空间,里面的键都是以字符串的形式保存,键指向的值可以是列表、字符串或者哈希表等等。
键的操作
对于键的操作主要有:
- set:添加或者更新键值对。
- del:删除键值对。
- get:获取值。
- flushdb:清空整个数据库。
- dbsize:获取键值对的数量。
- exists:查看键是否存在。
- rename:重命名键。
- keys:匹配某前缀的所有键。
读写键空间时的维护操作:
- 在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或者不命中次数。
- 在读取一个键之后,服务器会更新LRU时间。
- 在读取一个键之时,服务器会先判断键是否过期,如果过期,就会删除这个键,返回空值。
- 如果客户端使用watch指令监视某个键,那么之后发生了对该键的修改,这个键就会标记为脏,让事务程序注意到这个键被修改过了。
- 每次对一个键修改,都会对脏键计数器的值加一,这个计数器会触发服务器的持久化或者复制操作。
- 如果开启了数据库通知功能,那么对键进行修改之后,服务器将会按配置发送相应的数据库通知。
键的生存时间或过期时间
通过指令设置键的生存时间:
- expire:以秒为单位的生存时间。
- pexpire:以毫秒为单位的生存时间。
- expireat:设置以秒为单位的过期时间,时间戳。
- pexpireat:设置以毫秒为单位的过期时间。
其实上面的指令都会将转化为pexpireat来执行。
过期字典:
redisDb结构中存在一个expires字典,用于保存数据库中所有键的过期时间。
移除过期时间指令: persist
计算并返回剩余生存时间: TTL
过期键删除策略
有三种不同的策略:
- 定时立即删除
设置键的过期时间的同时,创建一个定时器,让定时器在过期时间来临的时候,立即执行删除。
对内存友好,对cpu不好,需要创建并维护大量的定时器,开销很大,不现实。 - 惰性删除
键过期了暂时不管,每次键被获取的时候,再去判断是不是该删除这个键。
对cpu友好,对内存不好,浪费大量的内存。 - 定期删除
每隔一段时间就对数据库进行一次检查,删除过期的键。
定期策略是前两种策略的折中。难点在于确定每次删除任务的时长和触发频率。
Redis过期键删除策略:
是配合惰性删除和定期删除的。
在获取键的时候,都会判断该键是否过期,如果过期了就将键删除并返回空。这就是惰性删除。
定期删除,每次运行时就会从数据库随机取出一定数量的键进行检查,删除过期键。全局变量中存储当前处理的进度,依次循环执行。
AOF、RDB和复制功能对过期键的处理
RDB
对于生成RDB文件:过期键不会保存到新创建的RDB文件中。
对于载入RDB文件:如果服务器以master运行,那么过期键就不会被载入到数据库中;对于slave节点,不论是否过期,都会被载入到数据库。
AOF
对于写入:如果某个键已经过期,但是还没有被删除,那么就不会产生任何影响;如果后来被删除了,那么就会向AOF文件追加一跳del指令删除该键。
对于重写:在执行AOF重写的过程中,程序会对数据库中的键进行检查,过期的键不会被重写到新AOF文件中。
复制
当服务器处于复制模式下,从服务器的过期键删除会由主服务器控制:
- 当master在删除一个过期键之后,会显式地向所有slave发送一个del指令,让slave删除这个过期键。
- slave只有在接收到master的del指令才会删除过期键,其他情况下都不会。
这样可以保持slave数据库的一致性。
数据通知
客户端可以订阅服务器相关的操作信息,从而得知数据库中键的变化以及数据中命令的执行情况。
RDB持久化
RDB特点
- 既可以手动,也可以根据服务器配置定期执行。
- 生成的RDB文件是压缩过的二进制文件,保存在磁盘中以实现持久化。
- 持久化记录的是数据本身。
RDB文件的创建和载入
手动创建文件的方法:
- save:直接阻塞服务器进程,直到RDB文件创建完毕。
- bgsave:创建一个子进程,让进程去创建RDB文件。在bgsave执行的时候,客户端发送的save和bgsave指令会被拒绝。
bgrewriteaof也不能与bgsave同时执行,如果正在执行bgsave,那么会在bgsave执行完毕后执行bgrewriteaof;如果正在执行bgrewriteaof,那么bgsave会被拒绝。
如果开启了AOF持久化,服务器优先使用AOF文件来还原数据。
服务器在载入RDB文件的时候,会一直处于阻塞,直到载入工作完成。
自动间隔保存
用户可以通过save选项设置多个保存条件,当任意一个保存条件被满足了,服务器就会执行bgsave命令。
触发保存的配置选项的模式是服务器在指定范围时间内,至少进行了多少次修改。
工作流程:
- 服务器维护一个dirty计数器和一个lastesave属性,分别保存着上次save操作之后数据库进行了多少次修改,上次保存的时间。例如一百毫秒或者100次修改。
- 当每次完成save指令之后,dirty和lastsave都会被初始化;
- 发生数据修改的时候,dirty就会增加;
- 服务器默认每隔100毫秒就会检查dirty和当前时间,当满足一项配置条件,那么就会执行bgsave的指令。
RDB文件结构
第六版的RDB文件。
一个完整的RDB文件包含五个部分:
- REDIS:长度为五个字节,保存着“REDIS”五个字符,用于表示该文件是RDB文件。
- db_version:长度为四个字节,是一个字符串表示的整数,记录当前RDB文件的版本号。
- database:包含任意个数的数据库,以及各个数据库中的键值对数据。如果数据库都是是空的,那么久没有这部分了。
- EOF:长度为一个字节,标志RDB文件正文内容结束。
- check_num:长度为八个字节,对前面的一个校验和。
databases
一个RDB文件可以保存任意多个非空的数据库。每个非空的数据库可以保存为如下三个部分:
- SELECTDB:长度为一个字节,当读入程序遇到这个值的时候,就直到接下来读入将是一个数据的号码。
- db_number:保存着一个数据库的号码,长度不定。读入db_number的时候,服务器就会调用select命令,切换数据库。
- key_value_pairs:保存数据库中所有的键值对,如果键值对由过期时间,那么过期时间也会被保存。
key_value_pairs
如果键值对有过期时间,那么由五个部分组成,而不带过期时间的键值在文件中由三部分组成:
- EXPIRETIEM_MS:告诉程序接了下来读入的是一个以毫秒为单位的过期时间。
- ms:表示一个以毫秒为单位的UNIX时间戳。
- TYPE:记录了value的类型,可以是字符串、列表、集合、有序结合、哈希表之类的。
- key:是一个字符串对象。
- value:更具TYPE类型的不同,值也不同。每个value都保存一个值对象,每个值对象类型都由TYPE记录,这些值就是上一篇文章中提到的各种对象。
AOF持久化
不同于RDB,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF持久化的实现原理
- 命令追加:当AOF持久化打开的时候,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。
- 文件写入:服务器在每次结束事件循环之前,它都会判断是否需要将aof_buf缓冲区写入和保存到AOF文件。
- 文件同步:文件写入的时候可能只会将数据保存到缓冲区,超过一定事件之后才会将缓冲区的数据写到磁盘。通过调用同步函数,可以强制让系统将缓冲区的数据写到磁盘。
在数据安全和性能之间需要权衡,系统提供不同的缓冲区写入策略,比如每个循环,每个事件。
AOF文件载入与数据还原
服务器只需要执行AOF文件就能实现数据的还原。
Redis读取AOF文件并还原数据的步骤如下:
- 创建一个不带网络连接的伪客户端;
- 从AOF文件分析并读取一条命令;
- 只用伪客户端执行被读出的写命令;
- 直到命令处理完位置。
AOF重写
随着服务器的运行时间边长,AOF文件的体积会越来越大,这时候需要通过AOF文件重写的功能,创建一份新的AOF文件,将老的AOF文件复制过去,并去掉冗余的部分。这样体积就会小很多。
在执行BGREWRITEAOF命令时,服务器会维护一个重写缓冲区,该缓冲区会在子进程创建AOF文件期间,记录服务器所有的写命令,当进程完成AOF文件的工作之后,服务器会将重写缓冲区内容追加到AOF文件的末尾,最后用新AOF文件代替旧的。
事件
服务器需要处理的事件分为两类:
- 文件事件:文件事件就是服务器对套接字操作的抽象,服务器与客户端的通信会产生相应的文件事件,而服务器通过监听并处理这些事件来完成一系列网络操作。
- 时间事件:需要在给点的时间点完成的操作的抽象。
文件事件
Redis基于Reactor模式开发自己的网络事件处理器,文件事件处理器工作原理:
- 使用IO多路复用程序来监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好连接应带、读取、写入、关闭等操作的时候,相应的文件事件就会产生,然后文件事件处理器就用之前关联好的事件处理器来处理这些事件。
文件事件处理的构成:
- 套接字
- IO多路复用程序
将多个套接字收入一个队列,通过队列,有序、同步、依次为文件事件分派器传送套接字,当前这个套接字产生的事件处理完毕,再送下一个。 - 文件事件分派器
接收套接字,并根据套接字调用相应的事件处理器。 - 事件处理器
定义了相应事件下,服务器的相应动作。
时间事件
Redis的时间事件分为两类:
- 定时事件
- 周期性事件
时间事件组成属性:
- id:递增。
- when:记录时间事件的到达时间,毫秒精度。
- timeProc:时间时间处理器。
时间事件工作原理:
服务器将所有事件都放在一个时间无序的链表中,每当时间事件处理器运行的时候,就遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器。
事件调度
事件调度流程如下图所示。
可以看出,事件之间处理时同步、有序、原子地进行的,事件不会被中断,所以,事件中需要尽可能减少阻塞的时间,并在有需要的时候主动让出执行权。
从这里可以看出,时间事件处理通常会被设定的事件晚一些。
服务器
服务器的作用
- 负责与多个客户端建立网络连接。
- 处理客户端发送的命令请求。
- 在数据库中保存客户端执行命令所产生的数据。
- 通过资源管理来维持服务器的运转。
服务器执行过程
以SET命令为例:
- 客户端向服务器发送命令请求
这里客户端需要将用户的命令转换协议。 - 服务器处理命令,并操作数据库,再产回复命令
先将命令放到缓冲区,然后依次处理分析,再调用命令执行器。 - 服务器将执行回复发送给客户端
将输出缓冲区的回复数据发送给客户端。 - 客户端接收回复,打印
客户端需要协议转换。
命令执行器的工作原理:
- 查找命令实现:在命令表中朝朝参数所指定的命令,并找到命令保存到客户端状态的cmd属性里面。命令表是一个字典,字典的键就是命令的名字,字典的值就是命令的实现信息。
- 执行预备操作:检查参数是否正确,身份是否合法,是否处于订阅模式,服务器是否能够发送数据之类的。
- 调用命令的实现函数:将客户端状态的指针作为参数传入实现函数,并将回复放在输出缓冲区中。
- 执行后续工作:如果开启了慢查询日志,慢查询日志模块会检查是否需要为刚刚执行的命令添加一条慢查询日志;存储刚才执行所耗费的时长和调用计数;如果开启了AOF,那么就会将命令请求放到AOF缓冲区;如果其他从服务器正在复制当前这个服务器,那么这个命令也会给其他的从服务器。
serverCron函数
Redis服务器中的serverCron函数默认每个100毫秒执行依次,负责管理服务器的资源并维护服务器自身的运转。
serverCron函数功能如下:
- 更新服务器时间缓存
服务器中不少功能需要获取当前的系统时间,为了减少系统调用次数,服务器中会缓存当前时间,然后由serverCron函数进行更新维护。可以看出,这个时间精度不高,所以,只有在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务等事件精度要求不高的功能上使用时间缓存;而设置过期时间、添加慢查询日志等高精度要求的功能来说,就会通过系统调用获取当前准确的时间。 - 更新LRU时钟
服务器状态中的lruclock属性保存服务器的LRU时钟,也是时间缓存的一种。每个对象都会由一个lru属性,用于保存最后一次被访问的时间。每十秒更新一次。 - 更新服务器每秒执行命令次数
估算平均值来计算得出的。 - 更新服务器内存峰值记录
- 处理SIGTERM信号
在启动服务器的时候,Redis会为服务器进程的SIGTERM信号关联处理器函数,每次serverCron函数运行的时候,都会根据属性值决定是否关闭服务器。 - 管理客户端资源
如果客户端与服务器连接已经超时,客户端没有互动,那么程序就会释放这个客户端。
如果客户端在上次执行命令请求之后,输入缓冲区超过了一定长度,那么程序就会释放客户端当前的输入缓冲区。 - 管理数据库资源
会对数据库进行检查,删除过期键,可能对字典进行收缩。 - 执行被延迟的BGREWRITEAOF
上面提到的AOF重写是分次的,就会在主循环中渐进执行。 - 检查持久化操作的运行
检查RDB和AOF的子进程,如果有信号到达,说明RDB文件或者AOF重写已经完成了,服务器需要进行后续操作。
如果没有子进程在运行,就会检查是否需要AOF重写,是否需要自动保存。 - 将AOF缓冲区内容写入AOF文件
- 关闭异步客户端
- 增加cronloops计数器的值,用于记录serverCron执行的次数。
初始化服务器
- 初始化服务器状态结构
- 载入配置选项
- 初始化服务器数据结构
- 还原数据库状态
- 执行时间循环