第二部分 单机数据库的实现
一、数据库
1.1 服务器中的数据库
redis所有数据库都在redis.h/redisServer就够的db数组里面,db数组的每一个项都是redis.h/redisDb就够,每个redisDb结构代表一个数据库。初始化的时候,程序根据redisServer中服务器状态dbnum来决定创建多少个数据库,默认16个。
1.2 切换数据库
客服端使用SELECT命令来切换数据库 SELECT 2表示切换到2号数据库,其中redisClient结构的db属性记录了客服端的目标数据库,是一个指针。
指向后的客户端和服务器的关系,通过修改这个指针就可以选择不同的数据库,这就是SELECT命令的实现原理。
1.3 数据库键空间
redisDb结构里面的dict字典保存了数据库的所有键值对,叫键空间。
- 键空间的键也就是数据库的键,每个键是一个字符串对象
- 键空间的值也就是数据库的值,值可以是字符串对象、列表对象等五种对象的一种
对数据库的增删改查都是通过对键空间进行操作来实现的,下图展示了数据库键空间的样子
其中简单的增删改查都是直接在这个键空间里面操作的,通过键获取值。其他的一些命令都是通过键空间来操作实现的。- 清空数据库的FLUSHDB命令,删除所有的键
- 返回随机某个键的RANDOMKEY命令,随机返回一个键
- 返回数据库数量的DBSIZE,返回包含的键值对的数量
- 还有EXISTS、RENAME、KEYS等
1.4 设置键的生存时间或过期时间
- EXPIRE或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间,经过指定时间后,服务器就会自动删除生存时间为0的键。
注意:SETEX命令可以在设置一个字符串键的同事设置过期时间,只能作用于字符串 - EXPIREAT或者PEXPIREAT命令,以秒或者毫秒精度给数据库某个键设置过期时间 键的过期时间来临时,服务器会自动从数据库中删除这个键
- TTL命令或者PTTL命令接收一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间,返回距离这个键被服务器自动删除还有多长时间
1.4.1设置过期时间
- EXPIRE命令用于将键key的生存时间设置为ttl秒
- PEXPIRE命令用于将键key的生存时间设置为ttl毫秒
- EXPIREAT命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳
- PEXPIREAT命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳
前面三种最后都是使用PEXPIREAT命令来执行的
1.4.2保存过期时间
redisDb结构的expire字典保存了数据库中所有键的过期时间,叫过期字典
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象
- 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间—一个毫秒精度的UNIX时间戳
PERSIST命令可以移除一个键的过期时间
1.4.3 过期键的判定和删除策略
判定:
- 检查给定键是否存在于过期字典:存在,那么取得键的过期时间
- 检查当前UNIX时间戳是否大于键的过期时间:如果是,那么键已经过期;否则未过期
删除策略:
定时删除、惰性删除、定期删除(一三为主动删除,二位被动删除)
- 定时删除:
设置键的过期时间的同事,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
缺点:对CPU不友好,过期键很多的情况时。会用到时间事件,时间事件是无序链表,查找为O(N)----并不能高效处理大量时间事件。 - 惰性删除:
放任键过期不管,从键空间获取键时,都检查取得的键是否过期,过期就删除,没过期就返回。
缺点: 对内存不友好,如果过期键一直不被删除,内存得不到释放
3.定期删除:每隔一段时间,程序检查数据库,删除里面过期键。是一种折中的删除策略。
1.4.4 定期删除策略的实现
定期删除策略由redis.c/activeExpireCycle函数实现,当redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,在规定时间内,分多次遍历各个数据库,从数据库的expire字典中随机检查一部分键的过期时间,并删除过期键
1.5.小结
二、RDB持久化
RDB持久化功能生出的RDB文件是一个经过压缩的二进制文件,通过这个文件还可以还原生成RDB文件时的数据库状态。
2.1 RDB文件的创建于载入
通过SAVE和BGSAVE命令可以生成RDB文件
- SAVE命令会阻塞redis服务器进程,知道RDB文件创建完毕,在服务器阻塞期间,服务器不能处理任何命令请求。
- BGSAVE命令会派生一个子进程,子进程创建RDB文件,服务器进程继续处理命令请求。
服务器在启动时检查到RDB文件存在,就会自动载入。因为AOF文件的更新频率通常比RDB文件的频率高,所以会优先使用AOF文件来还原数据库状态
2.2 自动间隔性保存
redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
save 900 1
服务器在900秒之内,对数据库至少1次修改就会执行BGSAVE命令。这个命令可以设置多个
2.2.1 设置保存条件
服务器会根据save选项所设置的保存条件,设置服务器状态redisServer结构saveparams属性
2.2.2 dirty计数器和lastsave属性
- dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改
- lastsave属性是一个UNIX时间戳,记录服务器上一次成功执行SAVE命令或者BGSAVE命令的时间
- 下图的dirty为123,表示服务器上次保存之后,对数据库状态进行了123次修改
- lastsave属性记录服务器上次执行保存操作的时间
2.2.3 检查保存条件是否满足
redis的服务器周期性操作函数serverCron默认每个100毫秒就会执行一次,该函数的其中一项工作就是检查save选项设置的保存条件是否满足,满足就执行。
2.3 RDB文件结构
全大写单词都是标示常量,全小写单词标示变量和数据。
- REDIS:5字节,程序通过这5个字节,快速检车所载入的文件是否是RDB文件。
- de_version:4字节,记录RDB文件的版本号
- database:包含0个或任意多个数据库,以及各个数据库中的键值对数据。
- EOF常量:1字节,标志着RDB文件正文内容的结束
- check_sum:是一个8字节长的无符号整数,保存着一个校验和,这个校验和是通过前面四个部分的内容进行计算得出的。服务器载入RDB文件时,会将载入数据所计算得出的校验和与check_sum记录的进行对比,来检查RDB文件是否又出差或者损坏。
2.3.1 database部分
保存非空数据库,0号和3号数据库非空,database 0代表0号数据库中的所有键值对数据
非空数据库在RDB文件中都可以保存以下三个部分:
- SELECTDB常量的长度为1字节,程序读入这个值的时候就知道接下来要读的是一个数据库号码。
- db_number保存一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节,2字节或者5字节。当读入db_number部分后,服务器调用SELECT命令进行数据库切换
- key_value_paris部分保存了数据库的所有键值对数据,过期时间也会和键值对保存在一起。
一个完整的RDB文件例子
2.3.2 key_value_paris部分
RDB的每个key_value_paris部分都保存了一个或以上的键值对,有过期时间,过期时间也会保存在内。
上图是不带有过期时间的键值对,下图是带有过期时间的键值对
- EXPIRETIME_MS常量1字节,告诉程序接下来读入的是一个以毫秒为单位的过期时间
- ms是一个8字节长的带符号整数,记录一个以毫秒为单位的UNIX时间戳,即过期时间
2.3.3 value的编码
value保存一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构。长度也会有所不同。这个部分和第一部分底层数据结构的选择类似。即键对应的值的类型选择,具体不详述。
2.4 小结
三、AOF持久化
与RDB通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存redis服务器所执行的写命令来记录数据库状态的。
3.1 AOF持久化的实现
分为命令追加,文件写入,文件同步三个步骤。
命令追加:
当AOF持久化功能打开时,服务器在执行一个命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
文件写入,文件同步:
redis服务器进程是一个时间循环,这个循环的文件时间负责接收客户端的命令请求,以及向客户端发送命令回复,而时间时间则负责执行像severCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时会执行写命令,导致一些内容追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区的内容写入和保存到AOF文件里面。
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定。
appendfsync的默认选项是everysec。
3.2 AOF文件的载入与数据还原
- 创建一个不带网络链接的伪客户端:伪客户端执行命令的效果和带网络连接的客户端执行命令的效果一样
- 从AOF文件中分析并读取一条写命令
- 使用伪客户端执行被读出的写命令
- 一直执行步骤2和3,直到AOF所有的写命令都被处理完毕
3.3 AOF重写
AOF是记录写命令的,随着时间流逝,AOF文件内容越来越多,体积越来越大,使用AOF对数据库还原的时间就越多。
redis可以创建一个新的AOF文件来代替现有的AOF文件,新旧两个文件所保存的数据库状态相同,新的AOF文件不会包含任何浪费空间的冗余操作,所以新的AOF文件的提及通常比旧的AOF文件体积小得多。
AOF重写的实现
AOF文件重写并不需要对现有的AOF文件进行任何读写、分析或者写入操作,这个功能是通过读取服务器当前的服务器状态来实现的
- 如果连续一条一条的插入数据到list集合,插入四条数据,AOF就要保存四条写命令。这个时候直接从数据库中读取键list的值,然后用一条命令来代替AOF文件中的四条命令,这就是AOF功能的实现原理。
redis如果直接使用aof_rewrite函数的话,那么从写AOF文件期间,服务器无法处理客户端发来的命令请求,所以redis为AOF重写程序建一个子进程。
- 子进程进行AOF重写期间内,服务器进程可以继续处理命令请求
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据的安全性。
问题: 子进程重写AOF期间,服务器处理新的命令可能会对现有的数据库状态进行修改,从而导致服务器当前状态和重写后的AOF文件所保存的数据库状态不一致。
解决: 设置一个重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,redis服务器执行一个写命令后,同时将这个命令发送给AOF缓冲区和AOF重写缓冲区
这样可以保证:
- AOF缓冲区的内容会被定期写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
- 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号后,会调用一个信号函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新AOF文件改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
在整个处理过程中,只有信号处理函数执行时会对服务器进程造成阻塞