目录
9 数据库
本章对Redis服务器的数据库实现进行详细介绍,说明服务器保存数据库的方法,客户端切换数据库的方法,数据库保存键值对的方法,以及针对数据库的添加、删除、查看、更新操作的实现方法等。除此之外,本章还会说明服务器保存键的过期时间的方法,以及服务器自动删除过期键的方法。最后还会说明Redis2.8新引入的数据库通知功能的实现方法。
9.1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库。
Redis服务器默认会创建16个数据库。
9.2 切换数据库
默认情况下,Redis客户端得目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。
9.3 数据库键空间
Redis是一个键值对(key-value pair)数据库服务器,服务器中得每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)。
所以针对数据库的操作,实际上都是通过对键空间字典进行操作来实现的。
下面就是对数据库的添加、删除、更新、取值等操作的实现原理。
9.4 设置键的生存时间或过期时间
通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒之后,服务器就会自动删除生存时间为0的键。
客户端还可以通过EXPIEREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time)。过期时间是一个UNIX时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键。
TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间,也就是,返回距离这个键被服务器自动删除还有多长时间。
9.4.1 设置过期时间
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
实际上EXPIRE、PEXPIPE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的。
9.4.2 保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典。
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象。
- 过期字典的值是一个long long类型的整数。
在实际上,键空间的键和过期字典的键都指向同一个键对象,所以不会出现任何重复对象,也不会浪费任何空间。
9.4.3 移除过期时间
PERSIST命令可以移除一个键的过期时间,PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
9.4.4 计算并返回剩余生存时间
TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间。
9.4.5 过期键的判定
通过过期字典,程序可以用一下步骤检查一个给定键是否过期
1)检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
2)检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
实现过期键判定的另外一种方法是使用TTL命令或者PTTL命令,比如说,如果对某个键执行TTL命令,并且命令返回的值大于等于0,那么就说明改键未过期。但在实际中运用的方法为上述的方法,因为直接访问字典比执行一个命令稍微快一点。
9.5 过期键删除策略
如果一个键过期了,那么它什么时候会被删除呢?
Redis中有三种不同的删除策略:
第一种和第三种为主动删除策略,而第二种则为被动删除策略。
9.5.1 定时删除
定时删除策略:通过使用定时器,定时删除策略可以保证过期键尽可能快地被删除,并释放过期键所占用的内存。
对内存是最友好的,但是对CPU时间是最不有好的。
9.5.2 惰性删除
惰性删除策略:程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何CPU时间。
对CPU时间来说是最友好的,对内存是最不友好的。
9.5.3 定期删除
从上面的定义看,上述两种删除方式在单一使用时都有明显的缺陷:
- 定期删除占用太多CPU时间,影响服务器的响应时间和吞吐量。
- 惰性删除浪费太多内存,有内存泄漏的危险。
定期删除策略时前两种策略的一种整合和折中:
- 每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
- 通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
定期删除策略的难点是确定删除操作执行的时长和频率:
- 执行太频繁,或者执行的时间太长,退化成定时删除。
- 执行得太少,或者执行得时间太短,退化为惰性删除。
9.6 Redis得过期键删除策略
虽然讨论了三种过期键删除策略,Redis服务器实际使用的是惰性删除和定期删除两种策略:通过使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
9.6.1 惰性删除策略的实现
过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
- 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除。
- 如果输入键未过期,那么expireIfNeeded函数不做动作。
expireIfNeeded函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的输入键,从而避免命令接触到过期键。
9.6.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
9.7 AOF、RDB和复制功能对过期键的处理
探讨过期键对Redis服务器中其他模块的影响,看看RDB持久化功能、AOF持久化功能以及复制功能是如何处理数据库中的过期键的。
9.7.1 生成RDB文件
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
9.7.2 载入RDB文件
在启动Redis服务器时,如果服务器开始了RDB功能,那么服务器将对RDB文件进行载入:
- 如果服务器以主服务器模式运行,那么在载入RDB文件时,未过期的键被载入到数据库,而过期键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影响。
- 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,无论是否过期,都会被载入到数据库中。
不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
9.7.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。
9.7.4 AOF重写
和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
9.7.5 复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键后,显示地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。
这种操作保证了主从服务器的一致性。
9.8 数据库通知
数据库通知是Redis2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
关注“某个键执行了什么命令”的通知称为键空间通知(key-space notification),还有一类称为键事件通知(key-event nofication)的通知,它们关注的是“某个命令被什么键执行了”。
9.8.1 发送通知
发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的。
9.8.2 发送通知的实现
9.9 重点回顾
- Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisServer.dbnum属性保存。
- 客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库。
- 数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。
- 因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
- 数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
- expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。
- Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
- 执行SAVE命令或者BGSAVE命令产生的新RDB文件不会包含已经过期的键。
- 执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。
- 当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显示地删除过期键。
- 当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显示地删除过期键。
- 从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
- 当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。
10 RDB持久化
Redis是一个键值对数据库服务器,我们将服务器中的非空数据库以及它们的键值对统称为数据库状态。
因为Redis是内存数据库,一旦服务器进程退出,服务器中的数据库状态也会消失不见。为了解决这个问题,Redis提供了RDB持久化功能,这个功能可以将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件。
10.1 RDB文件的创建与载入
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。
- SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
- BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
10.2 自动间隔性保存
因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
10.2.1 设置保存条件
当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:
接着,服务器程序会根据save选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性:
10.2.2 dirty计数器和lastsave属性
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性:
- dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
- lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
10.2.3 检查保存条件是否满足
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。
10.3 RDB文件结构
- REDIS:长度为5字节,保存着“REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。
- db_version:长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号。
- databases:包含着零个或任意多个数据库,以及各个数据库中的键值对数据。
- EOF:长度为1字节,这个常量标志着RDB文件正文内容的结束。
- check_sum:一个8字节长的字符号整数,保存着一个校验和。
10.3.1 databases部分
- SELECTDB:长度为1字节的常量,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。
- db_number:保存着一个数据库号码。
- key_value_pairs:保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。
图10-15展示了一个完整的RDB文件。文件包含了0号数据库和3号数据库。
10.3.2 key_value_pairs部分
RDB文件中每个key_value_pairs部分都保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。
10.3.3 value的编码
RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构、长度也会有所不同。
各种不同类型的值对象包括:
- 字符串对象
- 列表对象
- 集合对象
- 哈希表对象
- 有序集合对象
- INTSET编码的集合
- ZIPLIST编码的列表、哈希表或者有序集合
10.4 分析RDB文件
//详细请阅读参考用书
10.5 重点回顾
- RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据。
- SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
- BGSAVE命令由子进程执行保存操作,所以该命令不会阻塞服务器。
- 服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。
- RDB文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。
11 AOF持久化
除了RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Reids服务器所执行的写命令来记录数据库状态的。(类似与mysql中的binlog)
11.1 AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
11.1.1 命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
11.1.2 AOF文件的写入与同步
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接受客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可用以下伪代码表示:
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为如下表:
11.2 AOF文件的载入与数据还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。
11.3 AOF重写
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。
11.3.1 AOF文件重写的实现
实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能时通过读取服务器当前的数据库状态来实现的。
AOF重写功能的实现原理:
首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。
11.3.2 AOF后台重写
上述AOF文件重写使用的aof_rewrite函数会进行大量写入操作,这会造成线程长时间阻塞。所以Redis将AOF重写程序放在子进程里执行,这样可以同时达到两个目的:
- 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
使用子进程有一个问题需要解决:
子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
为了解决这种数据不一致问题:
Reids服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,如图:
这样一来,就可以保证:
- AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常工作。
- 从创建子程序开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这是新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
11.4 重点回顾
- AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
- AOF文件中的所有命令都以Redis命令请求协议的格式保存。
- 命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件。
- appendfsync选项的不同值对AOF持久化功能的安全性以及Redis服务器的性能有很大的影响。
- 服务器只要载入并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态。
- AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
- AOF重写是一个由歧义的名字,该功能时通过读取数据库中的键值对来实现的,程序无需对现有AOF文件进行任何读入、分析或者写入操作。
- 在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。
参考用书
《Redis设计与实现》,作者黄健宏。