第二部分 单机数据库的实现
第9章 数据库
9.2 切换数据库
SELECT命令的实现原理:通过修改 redisclient.db 指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能。
9.4 通过键的生存时间或过期时间
通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。
9.4.1 设置过期时间
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
EXPIRE <key> <ttl> 命令用于将键key的生存时间设置为ttl秒。
PRXPIRE <key> <ttl> 命令用于将键key的生存时间设置为ttl毫秒。
EXPIREAT <key> <timestramp> 命令用于将键key的过期时间设置为timestramp所指定的秒数时间戳。
PEXPIREAT <key> <timestramp> 命令用于将键key的过期时间设置为timestramp所指定的毫秒数时间戳。
虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE,PEXPIRE,EXPIREAT三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。
9.4.2 保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,称这个字典为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)。
- 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——个毫秒精度的UNIX时间戳。
9.4.5 过期键的判定
通过过期字典,程序可以用以下步骤检查一个给定键是否过期:
1)检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
2)检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
9.5 过期键删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
9.5.3 定期删除
- 定时删除占用太多CPU时间,影响服务器的响应时间和吞吐量。
- 惰性删除浪费太多内存,有内存泄漏的危险。
定期删除策略是前两种策略的一种整合和折中。
- 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
-通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
9.6 Redis的过期键删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略。
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持久化
10.1 RDB文件的创建与载入
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。
- SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
- 和SAVE命令直接阻塞服务器进程的做法不同,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
创建RDB文件的实际工作由rdb.c/rdbSave函数完成,SAVE命令和BGSAVE命会以不同的方式调用这个函数,通过以下伪代码可以明显地看出这两个命令之间的区别:
def SAVE () :
# 创建 RDB 文件
rdbSave()
def BGSRAVE () :
# 创建 子 进程
pid = Eork()
if pid == 0:
# 子 进程 负责 创建 RDB 文件
rdbSave ()
# 完成 之 后 向 进程 父 发 送信 和 号
signal_Parent ()
elif pid > 0:
# 父 进程 继续 处 理 命令 请 求 , 并 通过 轮 询 等 待 进程 子 的 信和 号
handle_request_and wait_signal()
else:
# 处 理 出 情况 错
handle_fork_error()
RDB文件的载入工作是在服务器启动时自动执行的。
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
10.1.1 SAVE命令执行时的服务器状态
当SAVE命令执行时,Redis服务器会被阻塞,所以当SAVE命令正在执行时,客户端所发送的所有命令请求都会被拒绝。
10.1.2 BGSAVE命令执行时的服务器状态
因为BGSAVE命令的保存工作是由子进程执行的,所以在子进程创建RDB文件的过程中,Redis服务器仍然可以继续处理客户端的命令请求,但是,在BGSAVE命令执行期间,服务器处理SAVE,BGSAVE,BGREWRITEAOF三个命令的方式会和平时有所不同。
- 首先,在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。
- 其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。
- 最后,BGREWRITEAOF和BGSAVE两个命令不能同时执行:
o 如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
o 如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
10.1.3 RDB文件载入时的服务器状态
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。
10.2 自动间隔性保存
save 900 1 # 表示900 秒内如果至少有 1 个 key 的值变化,则触发RDB
save 300 10 # 表示300 秒内如果至少有 10 个 key 的值变化,则触发RDB
save 60 10000 # 表示60 秒内如果至少有 10000 个 key 的值变化,则触发RDB
10.2.1 设置保存条件
当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:
save 900 1
save 300 10
save 60 10000
接着,服务器程序会根据save选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性。
saveparams属性是一个数组,数组中的每个元素都是一个saveparam结构,每个saveparam结构都保存了一个save选项设置的保存条件:
struct saveparam {
//秒数
time_t seconds;
//修改数
int changes;
};
比如说,如果save选项的值为以下条件:
save 900 1
save 300 10
save 60 10000
那么服务器状态中的saveparams数组将会是图10-6所示的样子。
10.2.2 dirty计数器和lastsave属性
除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性:
- dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
- lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
10.3 RDB文件结构
图 IMAGE_RDB_STRUCT_OVERVIEW 展示了一个完整 RDB 文件所包含的各个部分。图 IMAGE_RDB_STRUCT_OVERVIEW 中用全大写单词标示常量, 用全小写单词标示变量和数据。
RDB 文件的最开头是 REDIS 部分, 这个部分的长度为 5 字节, 保存着 “REDIS” 五个字符。 通过这五个字符, 程序可以在载入文件时, 快速检查所载入的文件是否 RDB 文件。
db_version 长度为 4 字节, 它的值是一个字符串表示的整数, 这个整数记录了 RDB 文件的版本号, 比如 “0006” 就代表 RDB 文件的版本为第六版。
databases 部分包含着零个或任意多个数据库, 以及各个数据库中的键值对数据:
- 如果服务器的数据库状态为空(所有数据库都是空的), 那么这个部分也为空, 长度为 0 字节。
- 如果服务器的数据库状态为非空(有至少一个数据库非空), 那么这个部分也为非空, 根据数据库所保存键值对的数量、类型和内容不同, 这个部分的长度也会有所不同。
EOF 常量的长度为 1 字节, 这个常量标志着 RDB 文件正文内容的结束, 当读入程序遇到这个值的时候, 它知道所有数据库的所有键值对都已经载入完毕了。
check_sum 是一个 8 字节长的无符号整数, 保存着一个校验和, 这个校验和是程序通过对 REDIS 、 db_version 、 databases 、 EOF 四个部分的内容进行计算得出的。 服务器在载入 RDB 文件时, 会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比, 以此来检查 RDB 文件是否有出错或者损坏的情况出现。
作为例子, 图 IMAGE_RDB_WITH_EMPTY_DATABASE 展示了一个 databases 部分为空的 RDB 文件: 文件开头的 “REDIS” 表示这是一个 RDB 文件, 之后的 “0006” 表示这是第六版的 RDB 文件, 因为 databases 为空, 所以版本号之后直接跟着 EOF 常量, 最后的 6265312314761917404 是文件的校验和。
10.3.1 databases 部分
一个 RDB 文件的 databases 部分可以保存任意多个非空数据库。
比如说, 如果服务器的 0 号数据库和 3 号数据库非空, 那么服务器将创建一个如图 IMAGE_RDB_WITH_TWO_DB 所示的 RDB 文件, 图中的 database 0 代表 0 号数据库中的所有键值对数据, 而 database 3 则代表 3 号数据库中的所有键值对数据。
每个非空数据库在 RDB 文件中都可以保存为 SELECTDB 、 db_number 、 key_value_pairs 三个部分, 如图 IMAGE_DATABASE_STRUCT_OF_RDB 所示。
SELECTDB 常量的长度为 1 字节, 当读入程序遇到这个值的时候, 它知道接下来要读入的将是一个数据库号码。
db_number 保存着一个数据库号码, 根据号码的大小不同, 这个部分的长度可以是 1 字节、 2 字节或者 5 字节。 当程序读入 db_number 部分之后, 服务器会调用 SELECT 命令, 根据读入的数据库号码进行数据库切换, 使得之后读入的键值对可以载入到正确的数据库中。
key_value_pairs 部分保存了数据库中的所有键值对数据, 如果键值对带有过期时间, 那么过期时间也会和键值对保存在一起。 根据键值对的数量、类型、内容、以及是否有过期时间等条件的不同, key_value_pairs 部分的长度也会有所不同。
作为例子, 图 IMAGE_EXAMPLE_OF_DB 展示了 RDB 文件中, 0 号数据库的结构。
另外, 图 IMAGE_RDB_WITH_DB_0_AND_DB_3 则展示了一个完整的 RDB 文件, 文件中包含了 0 号数据库和 3 号数据库。
10.3.2 key_value_pairs 部分
RDB 文件中的每个 key_value_pairs 部分都保存了一个或以上数量的键值对, 如果键值对带有过期时间的话, 那么键值对的过期时间也会被保存在内。
不带过期时间的键值对在 RDB 文件中对由 TYPE 、 key 、 value 三部分组成, 如图 IMAGE_KEY_WITHOUT_EXPIRE_TIME 所示。
带有过期时间的键值对在 RDB 文件中的结构如图 IMAGE_KEY_WITH_EXPIRE_TIME 所示。
EXPIRETIME_MS 和 ms , 它们的意义如下:
- EXPIRETIME_MS 常量的长度为 1 字节, 它告知读入程序, 接下来要读入的将是一个以毫秒为单位的过期时间。
- ms 是一个 8 字节长的带符号整数, 记录着一个以毫秒为单位的 UNIX 时间戳, 这个时间戳就是键值对的过期时间。
作为例子, 图 IMAGE_EXAMPLE_OF_KEY_WITHOUT_EXPIRE_TIME 展示了一个没有过期时间的字符串键值对。
图 IMAGE_EXAMPLE_OF_KEY_WITH_EXPIRE_TIME 展示了一个带有过期时间的集合键值对, 其中键的过期时间为 1388556000000 (2014 年 1 月 1 日零时)。
10.3.3 value 的编码
1.字符串对象
如果 TYPE 的值为 REDIS_RDB_TYPE_STRING , 那么 value 保存的就是一个字符串对象, 字符串对象的编码可以是 REDIS_ENCODING_INT 或者 REDIS_ENCODING_RAW 。
如果字符串对象的编码为 REDIS_ENCODING_INT , 那么说明对象中保存的是长度不超过 32 位的整数, 这种编码的对象将以图 IMAGE_INT_ENCODING_STRING 所示的结构保存。
如果字符串对象的编码为 REDIS_ENCODING_RAW , 那么说明对象所保存的是一个字符串值, 根据字符串长度的不同, 有压缩和不压缩两种方法来保存这个字符串:
- 如果字符串的长度小于等于 20 字节, 那么这个字符串会直接被原样保存。
- 如果字符串的长度大于 20 字节, 那么这个字符串会被压缩之后再保存。
注意
以上两个条件是在假设服务器打开了 RDB 文件压缩功能的情况下进行的, 如果服务器关闭了 RDB 文件压缩功能, 那么 RDB 程序总以无压缩的方式保存字符串值。
对于没有被压缩的字符串, RDB 程序会以图 IMAGE_NON_COMPRESS_STRING 所示的结构来保存该字符串。
其中, string 部分保存了字符串值本身,而 len 保存了字符串值的长度。
对于压缩后的字符串, RDB 程序会以图 IMAGE_COMPRESSED_STRING 所示的结构来保存该字符串。
其中, REDIS_RDB_ENC_LZF 常量标志着字符串已经被 LZF 算法压缩过了, 读入程序在碰到这个常量时, 会根据之后的 compressed_len 、 origin_len 和 compressed_string 三部分, 对字符串进行解压缩: 其中 compressed_len 记录的是字符串被压缩之后的长度, 而 origin_len 记录的是字符串原来的长度, compressed_string 记录的则是被压缩之后的字符串。
2.列表对象
如果 TYPE 的值为 REDIS_RDB_TYPE_LIST , 那么 value 保存的就是一个REDIS_ENCODING_LINKEDLIST 编码的列表对象, RDB 文件保存这种对象的结构如图 IMAGE_LINKEDLIST_ENCODING_LIST 所示。
list_length 记录了列表的长度, 它记录列表保存了多少个项(item), 读入程序可以通过这个长度知道自己应该读入多少个列表项。
图中以 item 开头的部分代表列表的项, 因为每个列表项都是一个字符串对象, 所以程序会以处理字符串对象的方式来保存和读入列表项。
作为示例, 图 IMAGE_EXAMPLE_OF_LINKEDLIST_ENCODING_LIST 展示了一个包含三个元素的列表。
结构中的第一个数字 3 是列表的长度, 之后跟着的分别是第一个列表项、第二个列表项和第三个列表项, 其中:
- 第一个列表项的长度为 5 , 内容为字符串 “hello” 。
- 第二个列表项的长度也为 5 , 内容为字符串 “world” 。
- 第三个列表项的长度为 1 , 内容为字符串 “!” 。
3.集合对象
如果 TYPE 的值为 REDIS_RDB_TYPE_SET , 那么 value 保存的就是一个 REDIS_ENCODING_HT 编码的集合对象, RDB 文件保存这种对象的结构如图 IMAGE_HT_ENCODING_SET 所示。
其中, set_size 是集合的大小, 它记录集合保存了多少个元素, 读入程序可以通过这个大小知道自己应该读入多少个集合元素。
图中以 elem 开头的部分代表集合的元素, 因为每个集合元素都是一个字符串对象, 所以程序会以处理字符串对象的方式来保存和读入集合元素。
4.哈希表对象
如果 TYPE 的值为 REDIS_RDB_TYPE_HASH , 那么 value 保存的就是一个 REDIS_ENCODING_HT 编码的集合对象, RDB 文件保存这种对象的结构如图 IMAGE_HT_HASH 所示:
- hash_size 记录了哈希表的大小, 也即是这个哈希表保存了多少键值对, 读入程序可以通过这个大小知道自己应该读入多少个键值对。
- 以 key_value_pair 开头的部分代表哈希表中的键值对, 键值对的键和值都是字符串对象, 所以程序会以处理字符串对象的方式来保存和读入键值对。
结构中的每个键值对都以键紧挨着值的方式排列在一起, 如图 IMAGE_KEY_VALUE_PAIR_OF_HT_HASH 所示。
因此, 从更详细的角度看, 图 IMAGE_HT_HASH 所展示的结构可以进一步修改为图 IMAGE_DETIAL_HT_HASH 。
作为示例, 图 IMAGE_EXAMPLE_OF_HT_HASH 展示了一个包含两个键值对的哈希表。
在这个示例结构中, 第一个数字 2 记录了哈希表的键值对数量, 之后跟着的是两个键值对:
- 第一个键值对的键是长度为 1 的字符串 “a” , 值是长度为 5 的字符串 “apple” 。
- 第二个键值对的键是长度为 1 的字符串 “b” , 值是长度为 6 的字符串 “banana” 。
5.有序集合对象
如果 TYPE 的值为 REDIS_RDB_TYPE_ZSET , 那么 value 保存的就是一个 REDIS_ENCODING_SKIPLIST 编码的有序集合对象, RDB 文件保存这种对象的结构如图 IMAGE_SKIPLIST_ZSET 所示。
sorted_set_size 记录了有序集合的大小, 也即是这个有序集合保存了多少元素, 读入程序需要根据这个值来决定应该读入多少有序集合元素。
以 element 开头的部分代表有序集合中的元素, 每个元素又分为成员(member)和分值(score)两部分, 成员是一个字符串对象, 分值则是一个 double 类型的浮点数, 程序在保存 RDB 文件时会先将分值转换成字符串对象, 然后再用保存字符串对象的方法将分值保存起来。
6.INTSET 编码的集合
如果 TYPE 的值为 REDIS_RDB_TYPE_SET_INTSET , 那么 value 保存的就是一个整数集合对象, RDB 文件保存这种对象的方法是, 先将整数集合转换为字符串对象, 然后将这个字符串对象保存到 RDB 文件里面。
如果程序在读入 RDB 文件的过程中, 碰到由整数集合对象转换成的字符串对象, 那么程序会根据 TYPE 值的指示, 先读入字符串对象, 再将这个字符串对象转换成原来的整数集合对象。
7.ZIPLIST 编码的列表、哈希表或者有序集合
如果 TYPE 的值为 REDIS_RDB_TYPE_LIST_ZIPLIST 、 REDIS_RDB_TYPE_HASH_ZIPLIST 或者 REDIS_RDB_TYPE_ZSET_ZIPLIST , 那么 value 保存的就是一个压缩列表对象, RDB 文件保存这种对象的方法是:
- 将压缩列表转换成一个字符串对象。
- 将转换所得的字符串对象保存到 RDB 文件。
如果程序在读入 RDB 文件的过程中, 碰到由压缩列表对象转换成的字符串对象, 那么程序会根据 TYPE 值的指示, 执行以下操作:
- 读入字符串对象,并将它转换成原来的压缩列表对象。
- 根据 TYPE 的值,设置压缩列表对象的类型: 如果 TYPE 的值为 REDIS_RDB_TYPE_LIST_ZIPLIST , 那么压缩列表对象的类型为列表; 如果 TYPE 的值为 REDIS_RDB_TYPE_HASH_ZIPLIST , 那么压缩列表对象的类型为哈希表; 如果 TYPE 的值为 REDIS_RDB_TYPE_ZSET_ZIPLIST , 那么压缩列表对象的类型为有序集合。
从步骤 2 可以看出, 由于 TYPE 的存在, 即使列表、哈希表和有序集合三种类型都使用压缩列表来保存, RDB 读入程序也总可以将读入并转换之后得出的压缩列表设置成原来的类型。
10.5 重点回顾
- RDB 文件用于保存和还原 Redis 服务器所有数据库中的所有键值对数据。
- SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
- BGSAVE 命令由子进程执行保存操作,所以该命令不会阻塞服务器。
- 服务器状态中会保存所有用 save 选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行 BGSAVE 命令。
- RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。
第11章 AOF持久化
除了RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保有Redis服务器所执行的写命令来记录数据库状态的,如图11-1所示。
11.1 AOF持久化的实现
AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
11.1.1 命令追加
当 AOF 持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾:
struct redisServer {
// ...
// AOF 缓冲区
sds aof_buf;
// ...
};
11.1.2 AOF 文件的写入与同步
Redis 的服务器进程就是一个事件循环(loop), 这个循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复, 而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 aof_buf 缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用 flushAppendOnlyFile 函数, 考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面.
flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定, 各个不同值产生的行为如表 TABLE_APPENDFSYNC 所示。
表 TABLE_APPENDFSYNC
appendfsync 选项的值 | flushAppendOnlyFile 函数的行为 |
---|---|
always | 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件。 |
everysec | 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是由一个线程专门负责执行的。 |
no | 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。 |
如果用户没有主动为 appendfsync 选项设置值, 那么 appendfsync 选项的默认值为 everysec。
11.2 AOF文件的载入与数据还原
Redis读取AOF文件并还原数据库状态的详细步骤如下:
- 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样。
- 从AOF文件中分析并读取出一条写命令。
- 使用伪客户端执行被读出的写命令。
- 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。
11.3 AOF重写
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。
11.3.1 AOF文件重写的实现
虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为"AOF文件重写",但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。
11.3.2 AOF后台重写
上面介绍的AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用aof-rewrite函数的话,那么在重写AOF文件期间,服务期将无法处理客户端发来的命令请求。
很明显,作为一种辅佐性的维护手段,Redis不希望AOF重写造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:
- 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。
在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:
1)执行客户端发来的命令。
2)将执行后的写命令追加到AOF缓冲区。
3)将执行后的写命令追加到AOF重写缓冲区。
这样一来可以保证:
- AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
- 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,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 文件重写操作。
第12章 事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
- 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
- 时间事件(time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
12.1 文件事件
Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler):
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
12.1.1 文件事件处理器的构成
图 IMAGE_CONSTRUCT_OF_FILE_EVENT_HANDLER 展示了文件事件处理器的四个组成部分, 它们分别是套接字、 I/O 多路复用程序、 文件事件分派器(dispatcher)、 以及事件处理器。
文件事件是对套接字操作的抽象, 每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时, 就会产生一个文件事件。 因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。
I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
尽管多个文件事件可能会并发地出现, 但 I/O 多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列, 以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字: 当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。
文件事件分派器接收 I/O 多路复用程序传来的套接字, 并根据套接字产生的事件的类型, 调用相应的事件处理器。
服务器会为执行不同任务的套接字关联不同的事件处理器, 这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。
12.1.3 事件的类型
I/O 多路复用程序可以监听多个套接字的 ae.h/AE_READABLE 事件和 ae.h/AE_WRITABLE 事件, 这两类事件和套接字操作之间的对应关系如下:
- 当套接字变得可读时(客户端对套接字执行 write 操作,或者执行 close 操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect 操作), 套接字产生 AE_READABLE 事件。
- 当套接字变得可写时(客户端对套接字执行 read 操作), 套接字产生 AE_WRITABLE 事件。
I/O 多路复用程序允许服务器同时监听套接字的 AE_READABLE 事件和 AE_WRITABLE 事件, 如果一个套接字同时产生了这两种事件, 那么文件事件分派器会优先处理 AE_READABLE 事件, 等到 AE_READABLE 事件处理完之后, 才处理 AE_WRITABLE 事件。
这也就是说, 如果一个套接字又可读又可写的话, 那么服务器将先读套接字, 后写套接字。
12.1.5 文件事件的处理器
Redis 为文件事件编写了多个处理器, 这些事件处理器分别用于实现不同的网络通讯需求, 比如说:
- 为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联连接应答处理器。
- 为了接收客户端传来的命令请求, 服务器要为客户端套接字关联命令请求处理器。
- 为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令回复处理器。
- 当主服务器和从服务器进行复制操作时, 主从服务器都需要关联特别为复制功能编写的复制处理器。
- 等等。
在这些事件处理器里面, 服务器最常用的要数与客户端进行通信的连接应答处理器、 命令请求处理器和命令回复处理器。
一次完整的客户端与服务器连接事件示例
假设一个 Redis 服务器正在运作, 那么这个服务器的监听套接字的 AE_READABLE 事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器。
如果这时有一个 Redis 客户端向服务器发起连接, 那么监听套接字将产生 AE_READABLE 事件, 触发连接应答处理器执行: 处理器会对客户端的连接请求进行应答, 然后创建客户端套接字, 以及客户端状态, 并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联, 使得客户端可以向主服务器发送命令请求。
之后, 假设客户端向主服务器发送一个命令请求, 那么客户端套接字将产生 AE_READABLE 事件, 引发命令请求处理器执行, 处理器读取客户端的命令内容, 然后传给相关程序去执行。
执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联: 当客户端尝试读取命令回复的时候, 客户端套接字将产生 AE_WRITABLE 事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。
12.2 时间事件
Redis的时间事件分为以下两类:
- 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序x在当前时间的30毫秒之后执行一次。
- 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔30毫秒就执行一次。
一个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的全局唯一ID(标识号),ID号按从小到大的顺序递增新事件的ID号比旧事件的ID号要大。
- when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间。
- timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:
- 如果事件处理器返回ae.h/AE-NOMORE,那么这个事件为定时事件:该事件在达到一次之后就会被删除,之后不再到达。
- 如果事件处理器返回一个非AE-NOMORE的整数值,那么这个事件为周期性时间:
12.2.3 时间事件应用实例: serverCron函数
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,它的主要工作包括:
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
- 清理数据库中的过期键值对。
- 关闭和清理连接失效的客户端。
- 尝试进行AOF或RDB持久化操作。
- 如果服务器是主服务器,那么对从服务器进行定期同步。
- 如果处于集群模式,对集群进行定期同步和连接测试。
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,servercron就会执行一次,直到服务器关闭为止。
12.3 事件的调度与执行
表12-1 记录了一次完整的事件调度和执行过程。
表12-1记录的事件执行过程凸显了上面列举的事件调度规则中的规则2、3、4:
- 因为时间事件尚未到达,所以在处理时间事件之前,服务器已经等待并处理了两次文件事件。
- 因为处理事件的过程中不会出现抢占,所以实际处理时间事件的时间比预定的100毫秒慢了30毫秒。
12.4 重点回顾
- Redis 服务器是一个事件驱动程序, 服务器处理的事件分为时间事件和文件事件两类。
- 文件事件处理器是基于 Reactor 模式实现的网络通讯程序。
- 文件事件是对套接字操作的抽象: 每次套接字变得可应答(acceptable)、可写(writable)或者可读(readable)时, 相应的文件事件就会产生。
- 文件事件分为 AE_READABLE 事件(读事件)和 AE_WRITABLE 事件(写事件)两类。
- 时间事件分为定时事件和周期性事件: 定时事件只在指定的时间达到一次, 而周期性事件则每隔一段时间到达一次。
- 服务器在一般情况下只执行 serverCron 函数一个时间事件, 并且这个事件是周期性事件。
- 文件事件和时间事件之间是合作关系, 服务器会轮流处理这两种事件, 并且处理事件的过程中也不会进行抢占。
- 时间事件的实际处理时间通常会比设定的到达时间晚一些。
第13章 客户端
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisclient结构(客户端状态),这个结构保存了客户端当前的状态信息。
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients链表来完成。
作为例子,图13-1展示了一个与三个客户端进行连接的服务器,而图13-2则展示了这个服务器的clients链表的样子。
13.1 客户端属性
客户端状态包含的属性可以分为两类:
- 一类是比较通用的属性, 这些属性很少与特定功能相关, 无论客户端执行的是什么工作, 它们都要用到这些属性。
- 另外一类是和特定功能相关的属性, 比如操作数据库时需要用到的 db 属性和 dictid 属性, 执行事务时需要用到的 mstate 属性, 以及执行 WATCH 命令时需要用到的 watched_keys 属性, 等等。
13.1.1 套接字描述符
客户端状态的 fd 属性记录了客户端正在使用的套接字描述符。根据客户端类型的不同, fd 属性的值可以是 -1 或者是大于 -1 的整数:
- 伪客户端(fake client)的 fd 属性的值为 -1 : 伪客户端处理的命令请求来源于 AOF 文件或者 Lua 脚本,而不是网络, 所以这种客户端不需要套接字连接, 自然也不需要记录套接字描述符。 目前 Redis 服务器会在两个地方用到伪客户端,一个用于载入 AOF 文件并还原数据库状态, 而另一个则用于执行 Lua 脚本中包含的 Redis 命令。
- 普通客户端的 fd 属性的值为大于 -1 的整数: 普通客户端使用套接字来与服务器进行通讯, 所以服务器会用 fd 属性来记录客户端套接字的描述符。 因为合法的套接字描述符不能是 -1 , 所以普通客户端的套接字描述符的值必然是大于 -1 的整数。
13.1.2 名字
在默认情况下, 一个连接到服务器的客户端是没有名字的。
13.1.3 标志
客户端的标志属性 flags 记录了客户端的角色(role), 以及客户端目前所处的状态。每个标志使用一个常量表示, 一部分标志记录了客户端的角色:
- 在主从服务器进行复制操作时, 主服务器会成为从服务器的客户端, 而从服务器也会成为主服务器的客户端。 REDIS_MASTER标志表示客户端代表的是一个主服务器, REDIS_SLAVE 标志表示客户端代表的是一个从服务器。
- REDIS_PRE_PSYNC 标志表示客户端代表的是一个版本低于 Redis 2.8 的从服务器, 主服务器不能使用 PSYNC 命令与这个从服务器进行同步。 这个标志只能在 REDIS_SLAVE 标志处于打开状态时使用。
- REDIS_LUA_CLIENT 标识表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端。
13.1.4 输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求。输入缓冲区的大小会根据输入内容动态地缩小或者扩大, 但它的最大大小不能超过 1 GB , 否则服务器将关闭这个客户端。
13.1.5 命令与命令参数
在服务器将客户端发送的命令请求保存到客户端状态的 querybuf 属性之后, 服务器将对命令请求的内容进行分析, 并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv 属性和 argc 属性。
- argv 属性是一个数组, 数组中的每个项都是一个字符串对象: 其中 argv[0] 是要执行的命令, 而之后的其他项则是传给命令的参数。
- argc 属性则负责记录 argv 数组的长度。
13.1.7 输出缓冲区
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面, 每个客户端都有两个输出缓冲区可用, 一个缓冲区的大小是固定的, 另一个缓冲区的大小是可变的:
- 固定大小的缓冲区用于保存那些长度比较小的回复, 比如 OK 、简短的字符串值、整数值、错误回复,等等。
- 可变大小的缓冲区用于保存那些长度比较大的回复, 比如一个非常长的字符串值, 一个由很多项组成的列表, 一个包含了很多元素的集合, 等等。
客户端的固定大小缓冲区由 buf 和 bufpos 两个属性组成:
typedef struct redisClient {
// ...
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
// ...
} redisClient;
buf 是一个大小为 REDIS_REPLY_CHUNK_BYTES 字节的字节数组, 而 bufpos 属性则记录了 buf 数组目前已使用的字节数量。
REDIS_REPLY_CHUNK_BYTES 常量目前的默认值为 16*1024 , 也即是说, buf 数组的默认大小为 16 KB 。
当 buf 数组的空间已经用完, 或者回复因为太大而没办法放进 buf 数组里面时, 服务器就会开始使用可变大小缓冲区。
可变大小缓冲区由 reply 链表和一个或多个字符串对象组成:
typedef struct redisClient {
// ...
list *reply;
// ...
} redisClient;
通过使用链表来连接多个字符串对象, 服务器可以为客户端保存一个非常长的命令回复, 而不必受到固定大小缓冲区 16 KB 大小的限制。
13.1.8 身份验证
客户端状态的 authenticated 属性用于记录客户端是否通过了身份验证。如果 authenticated 的值为 0 , 那么表示客户端未通过身份验证; 如果 authenticated 的值为 1 , 那么表示客户端已经通过了身份验证。
当客户端 authenticated 属性的值为 0 时, 除了 AUTH 命令之外, 客户端发送的所有其他命令都会被服务器拒绝执行。
authenticated 属性仅在服务器启用了身份验证功能时使用: 如果服务器没有启用身份验证功能的话, 那么即使 authenticated 属性的值为 0 (这是默认值), 服务器也不会拒绝执行客户端发送的命令请求。
13.1.9 时间
最后, 客户端还有几个和时间有关的属性:
typedef struct redisClient {
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
ctime 属性记录了创建客户端的时间, 这个时间可以用来计算客户端与服务器已经连接了多少秒 —— CLIENT_LIST 命令的 age 域记录了这个秒数。
lastinteraction 属性记录了客户端与服务器最后一次进行互动(interaction)的时间, 这里的互动可以是客户端向服务器发送命令请求, 也可以是服务器向客户端发送命令回复。
lastinteraction 属性可以用来计算客户端的空转(idle)时间, 也即是, 距离客户端与服务器最后一次进行互动以来, 已经过去了多少秒 —— CLIENT_LIST 命令的 idle 域记录了这个秒数。
obuf_soft_limit_reached_time 属性记录了输出缓冲区第一次到达软性限制(soft limit)的时间。
13.2 客户端的创建与关闭
13.2.1 创建普通客户端
如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器(在第12章有介绍),为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
13.2.2 关闭普通客户端
一个普通客户端可以因为多种原因而被关闭:
- 如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
- 如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会服务器关闭。
- 如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭。
- 如果用户为服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。不过timeout选项有一些例外情况:如果客户端是主服务器(打开了REDIS-MASTER标志),从服务器(打开了REDISSLAVE标志),正在被BLPOP等命令阻塞(打开了REDIS BLOCKED标志),或者正在执行SUBSCRIBE,PSUBSCRIBE等订阅命令,那么即使客户端的空转时间超过了timeout选项的值,客户端也不会被服务器关闭。
- 如果客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1GB),那么这个客户端会被服务器关闭。
- 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。
服务器使用两种模式来限制客户端输出缓冲区的大小:
- 硬性限制(hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
- 软性限制(soft limit):如果输出缓冲区的大小超过了软性限制所设置的大小,但还没超过硬性限制,那么服务器将使用客户端状态结构的obuf softlimit reached-time属性记录下客户端到达软性限制的起始时间;之后服务器会继续监视客户端,如果输出缓冲区的大小一直超出软性限制,并且持续时间超过服务器设定的时长,那么服务器将关闭客户端;相反地,如果输出缓冲区的大小在指定时间之内,不再超出软性限制,那么客户端就不会被关闭,并且obuf soft-1imit_reached_time属性的值也会被清零。
使用client-output-buffer-limit选项可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,该选项的格式为:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
以下是三个设置示例:
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
第一行设置将普通客户端的硬性限制和软性限制都设置为0,表示不限制客户端的输出缓冲区大小。
第二行设置将从服务器客户端的硬性限制设置为256 MB,而软性限制设置为64 MB,软性限制的时长为60秒。
第三行设置将执行发布与订阅功能的客户端的硬性限制设置为32 MB,软性限制设置为8MB,软性限制的时长为60秒。
13.2.3 Lua的伪客户端
服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua client属性中。
lua_client伪客户端在服务器运行的整个生命期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭。
13.2.4 AOF文件的伪客户端
服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端并在载入完成之后,关闭这个伪客户端。
13.3 重点回顾
- 服务器状态结构使用 clients 链表连接起多个客户端状态, 新添加的客户端状态会被放到链表的末尾。
- 客户端状态的 flags 属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态。
- 输入缓冲区记录了客户端发送的命令请求, 这个缓冲区的大小不能超过 1 GB 。
- 命令的参数和参数个数会被记录在客户端状态的 argv 和 argc 属性里面, 而 cmd 属性则记录了客户端要执行命令的实现函数。
- 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用, 其中固定大小缓冲区的最大大小为 16 KB , 而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
- 输出缓冲区限制值有两种, 如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭; 除此之外, 如果客户端在一定时间内, 一直超过服务器设置的软性限制, 那么客户端也会被关闭。
- 当一个客户端通过网络连接连上服务器时, 服务器会为这个客户端创建相应的客户端状态。 网络连接关闭、 发送了不合协议格式的命令请求、 成为 CLIENT_KILL 命令的目标、 空转时间超时、 输出缓冲区的大小超出限制, 以上这些原因都会造成客户端被关闭。
- 处理 Lua 脚本的伪客户端在服务器初始化时创建, 这个客户端会一直存在, 直到服务器关闭。
- 载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建, 载入工作完毕之后关闭。
第14章 服务器
14.1 命令请求的执行过程
一个命令请求从发送到获得回复的过程中, 客户端和服务器需要完成一系列操作。
举个例子, 如果我们使用客户端执行以下命令:
redis> SET KEY VALUE
OK
那么从客户端发送 SET KEY VALUE 命令到获得回复 OK 期间, 客户端和服务器共需要执行以下操作:
- 客户端向服务器发送命令请求 SET KEY VALUE 。
- 服务器接收并处理客户端发来的命令请求 SET KEY VALUE , 在数据库中进行设置操作, 并产生命令回复 OK 。
- 服务器将命令回复 OK 发送给客户端。
- 客户端接收服务器返回的命令回复 OK , 并将这个回复打印给用户观看。
14.1.1 发送命令请求
Redis 服务器的命令请求来自 Redis 客户端, 当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器, 如图 14-1 所示。
举个例子, 假设客户端执行命令:
SET KEY VALUE
那么客户端会将这个命令转换成协议:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
然后将这段协议内容发送给服务器。
14.1.2 读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面。
- 对输入缓冲区中的命令请求进行分析, 提取出命令请求中包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里面。
- 调用命令执行器, 执行客户端指定的命令。
14.4 重点回顾
- 一个命令请求从发送到完成主要包括以下步骤:
- 客户端将命令请求发送给服务器;
- 服务器读取命令请求,并分析出命令参数;
- 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
- 服务器将命令回复返回给客户端。
- serverCron 函数默认每隔 100 毫秒执行一次, 它的工作主要包括更新服务器状态信息, 处理服务器接收的 SIGTERM 信号, 管理客户端资源和数据库状态, 检查并执行持久化操作, 等等。
- 服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
- 初始化服务器状态;
- 载入服务器配置;
- 初始化服务器数据结构;
- 还原数据库状态;
- 执行事件循环。