Part II 单机数据库的实现
9. 数据库
9.1 服务器中的数据库
-
Redis服务器默认会创建16个数据库。默认情况下,Redis客户端的目标数据库为0号数据库。
-
客户端可以通过执行SELECT命令来切换目标数据库。
9.2 数据库键空间
-
RedisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)
-
因为数据库的键空间是一个字典,所以所有针对数据库的操作,实际上都是通过对键空间字典进行操作来实现的.
-
清空整个数据库的FLUSHDB命令,用于返回数据库键数量的DBSIZE命令,类似的命令还有EXISTS、RENAME、KEYS等,这些命令都是通过对键空间进行操作来实现的。
-
在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间
-
如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作
9.3 设置键的生存或过期时间
-
通过EXPIRE命令或者PEXPIRE命令,可以以秒或者毫秒精度为某个键设置生存时间(Time To Live,TTL)
-
SETEX命令可以在设置一个字符串键的同时为键设置过期时间,服务器会自动删除生存时间为0的键
-
redisDb结构的expires字典保存了数据库中所有键的过期时间,称为过期字典。
-
PERSIST命令可以移除一个键的过期时间,TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位
9.4 过期键删除策略
三种不同的删除策略:
-
定时删除:
-
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作
-
对内存友好,对CPU不友好。删除过期键这一行为可能会占用相当一部分CPU时间
-
-
惰性删除:
-
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
-
对CPU时间友好,对内存不友好。只要这个过期键不被访问,它所占用的内存就不会释放。
-
-
定期删除:
-
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
-
定期删除策略是前两种策略的一种整合和折中:每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
-
9.5 Redis的过期键删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略,通过配合使用这两种删除策略,可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
惰性删除策略:
-
Redis命令在执行之前都会调用expirelfNeeded函数对输入键进行检查:
-
如果输入键已经过期,那么expirelfNeeded函数将输入键从数据库中删除。
-
expirelfNeeded函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的输入键,从而避免命令接触到过期键。
-
定期删除策略:
-
activeExpireCycle函数就会被周期性调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
9.6 AOF、RDB和复制功能对过期键的处理
-
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时会进行检查,已过期的键不会被保存到新创建的RDB文件中。
-
AOF不会因为过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。
-
当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,保证主从服务器数据的一致性。
9.7 数据库通知
数据库通知是Redis 2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
-
当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。
-
关注“某个键执行了什么命令”的称为键空间通知,关注的是“某个命令被什么键执行了”是键事件通知
10. RDB持久化
-
RDB文件是一个经过压缩的二进制文件,用于保存和还原Redis服务器所有数据库中的所有键值对数据。
-
该文件可以还原生成RDB文件时的数据库状态,即使Redis服务器进程退出,因为RDB文件是保存在硬盘里面的。
10.1 RDB文件的创建与载入
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE
-
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕
-
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件
-
因为AOF文件的更新频率通常比RDB高,如果开启了AOF,那么会优先使用AOF文件来还原数据库状态。
10.2 自动间隔性保存
默认只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
-
服务器在900秒之内,对数据库进行了至少1次修改。
-
服务器在300秒之内,对数据库进行了至少10次修改。
-
服务器在60秒之内,对数据库进行了至少10000次修改。
服务器状态还维持着一个dirtyit数器,以及一个lastsave属性:
-
dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,对数据库状态进行了多少次修改。
-
lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
10.3 RDB文件结构
图10-10展示了一个完整RDB文件所包含的各个部分。
一个RDB文件的databases部分可以保存任意多个非空数据库。每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分,如图10-13所示。
SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。
RDB文件中的每个key_value_pairs部分都保存了一个或以上数量的键值对
-
如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。
-
不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成。
-
对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。
Redis本身带有RDB文件检查工具redis-check-dump
11. AOF持久化
Redis还提供了AOF(Append Only File)持久化功能。
与RDB通过保存数据库中的键值对不同,AOF是通过保存Redis服务器所执行的写命令来记录数据库状态的,如图11-1所示。
11.1 AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
-
当AOF打开时,在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
-
在服务器每次结束一个事件循环之前,都会考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面
-
当appendfsync的值为always时,效率最慢但是最安全。为everysec时,足够快,最多丢失一秒钟的命令数据
11.2 AOF重写
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。Redis会创建一个不带网络连接的伪客户端执行所有写命令。
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。
-
Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新AOF文件不会包含任何浪费空间的冗余命令
-
AOF文件重写并不需要对现有的AOF文件进行读取,而是读取服务器当前的数据库状态。
-
Redis将AOF重写程序放到子进程里执行,服务器进程(父进程)可以继续处理命令请求。使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
-
为了避免父子进程数据不一致,Redis设置了一个AOF重写缓冲区,写命令会同时发送给AOF缓冲区和AOF重写缓冲区。
-
当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾
-
最后服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。
-
以上就是AOF后台重写,也即是BGREWRITEAOF命令的实现原理。
12. 事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
-
文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
-
时间事件 (time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
12.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(fle event handler):
-
文件事件处理器使用I/O多路复用 (multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。保持了Redis内部单线程设计的简单性
-
文件事件处理器的四个组成部分分别是套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器。
-
文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答 (accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。
-
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。
-
在事件处理器里面,服务器最常用的要数与客户端进行通信的连接应答处理器、命令请求处理器和命令回复处理器。
12.2 时间事件
Redis的时间事件分为以下两类:
-
定时事件:让一段程序在指定的时间之后执行一次。比如让程序X在当前时间的30毫米之后执行一次。
-
周期性事件:让一段程序每隔指定时间就执行一次。比如让程序Y每隔30毫秒就执行一次。
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
-
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由serverCron函数负责执行。
-
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。在Redis2.6版本,服务器默认规定serverCron每秒运行10次,平均每间隔100毫秒运行一次。
12.3 事件的调度与执行
因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度。
对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
13. 客户端
Redis服务器是典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接。
-
通过使用由I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
-
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redisClient结构(客户端状态)
-
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构
13.1 客户端属性
-
客户端状态的fd属性记录了客户端正在使用的套接字描述符。
-
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的:
-
固定大小的缓冲区用于保存那些长度比较小的回复,比如OK、简短的字符串值、整数值、错误回复等等。
-
可变大小的缓冲区用于保存那些长度比较大的回复,比如一个非常长的字符串值,一个由很多项组成的列表等。
-
13.2 客户端的创建与关闭
-
如果客户端是通过网络连接与服务器进行连接的普通客户端,服务器就会调用连接事件处理器,将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
-
网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制,以上这些原因都会造成客户端被关闭。
-
处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
-
载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。
14. 服务器
-
一个命令请求从发送到完成主要包括以下步骤:
-
客户端将命令请求发送给服务器;
-
服务器读取命令请求,并分析出命令参数;
-
命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
-
服务器将命令回复返回给客户端。
-
-
serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。
-
服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
-
1)初始化服务器状态;2)载入服务器配置;3)初始化服务器数据结构;4)还原数据库状态;5)执行事件循环。
-