《Redis 设计与实现》读书笔记(二)
单机数据库实现
九、数据库
1.服务器中的数据库
一个redis服务器保存多个数据库。
struct redisServer {
//一个数组,多个数据库
redisDb *db;
}
当执行select 1
,就是切换数据库到db[1]
,具体就是会修改redisClient.db指针到redisServer.db[1]
2.数据库键空间
typedef struct redisDb{
dict *dict;//数据库键空间
dict *expires;//过期时间
}
这里的dict就是上面说的字典数据结构。
这个字典的key就是redis里面的key,每个key都是字符串对象
值就是数据库的值,可以是字符串对象,列表对象,哈希对象,集合对象,游戏集合对象。
3.键的过期时间
如果我们对一个键设置过期时间
redis就会在字典expires里面,加上key=过期的时间戳(精确到毫秒)。
执行ttl,redis会比较expires里面的时间戳和当前时间的差值,然后返回差值。
过期key的判定
- 当访问key时,redis会检查key是否过期,如果是,删除key,并报错key不存在
- 对于一直没有访问的key,redis会定期扫描expires里面的key,判定key是否过期,如果是,就删除key
十、RDB持久化
存在内存中的数据,称为数据库状态
持久化就是把数据库状态,保存为RDB文件,RDB文件是存在硬盘的。
1.客户端发起保存
执行命令save,bgsave,可以立刻把数据库状态保存为RDB文件。
- save命令是阻塞的,即执行过程中,服务器不能处理其他客户端的请求。
- bgsave是异步的,redis会启动一个新进程,把内存的数据都复制到新进程,然后执行保存数据到RDB文件的工作,而原进程就继续处理客户端的请求
2.服务端定期保存
redis也会定期执行保存操作
服务器的配置:
save 900 1
表示服务器在900秒内,对数据库执行了至少一次修改,服务器就会执行保存操作
如果有多个svae配置,它们直接的关系是或的关系,即满足其中一个,就会执行保存
struct redisServer{
long long dirty;
time_t lastsave
}
数据库对象中,有两个属性:
- dirty,记录距离上一次保存操作后,数据库执行了多少次修改。
- lastsave,上一次保存操作的时间戳
redis通过这两个属性来实现定期保存的机制
3.RDB文件结构
RDB文件由
REDIS db_version databases EOF cehcks_sum
构成
- redis是一个字符串,
- db_version是一个4字节的int类型,表示数据库的版本号
- databases 表示数据库数据
- EOF 1字节表示文件的结束
- check_sum表示前面的数据的md5
1.数据库数据
databases部分的构成:
SELECTDB db_number PAIRS
- SELECTDB是1字节,常量
- db_number 是数据库编号
- PAIRS是数据库数据里面的键值对
2.键值对
键值对构成
EXPIRETIME_MS ms TYPE KEY VALUE
- EXPIRETIME_MS 1字节常量,表示这个键有超时时间,可选
- ms 超时时间的时间戳
- TYPE 1字节常量,表示键的类型,redis会根据这个常量,来决定怎么解析后面的KEY和VALUE
- KEY 是一个字符串对象,存储方法和VALUE的字符串对象一样
- 值的对象
3.VALUE编码
redis会根据TYPE这个常量,来决定怎么读取VALUE的数据。
KEY肯定就是字符串类型了。
3.1字符串对象
如果TYPE=REDIS_ENCODING_STRING,表示这个对象是数值字符串对象
字符串对象有以下三种存储类型
int类型
构成:
ENCODING int
- ENCODING 1字节常量,表示int类型,例如16位还是32位
- int 数字
无压缩字符串
如果TYPE=REDIS_ENCODING_RAW,表示这个是普通字符串
len string
- len 字符串的长度
- string 字符串的值
压缩字符串
如果字符串的长度大于20字节,就会压缩字符串。
REDIS_RDB_ENC_LZF compressed_len origin_len compressed_string
- REDIS_RDB_ENC_LZF 1字节常量,压缩的算法
- compressed_len 压缩后的长度
- origin_len 原字符串长度
- compressed_string 压缩后的内容
问题:
程序怎么知道这是int类型,还是无压缩字符串,还是压缩字符串的?
3.2列表对象
当TYPE=REDIS_RDB_TYPE_LIST 表示这是一个列表对象
list_length item1 item2 itemN
- list_length 列表的长度
- item1-N 列表的元素,都是字符串对象
3.3集合对象
如果TYPE=REDIS_RDB_TYPE_SET 那么表示这是一个集合对象
set_size elem1 ... elemN
- set_size集合的长度
- elem1 表示集合的元素,字符串对象
3.4哈希表对象
如果TYPE=REDIS_RDB_TYPE_HASH 那么表示这是一个哈希表对象
hash_size key_value_pair1 。。。。。。key_value_pairN
- hash_size 哈希表的键值对数量
- key_value_pair1 键值对的值
键值对的构成
key1 value1 key2 value2
- key1 键值对的键,字符串对象
- value1 键值对的值,字符串对象
3.5 有序集合对象
如果TYPE=REDIS_RDB_TYPE_ZSET,表示这是一个有序集合对象
sorted_set_size element1 。。。。 elementN
- sorted_set_size元素的数量
- element1 元素
每个元素的构成
member1 score1
- member1 元素的内容,字符串对象
- score1 元素的分值,redis会把int类型或者float类型转换为字符串类型保存
4.RDB文件例子
REDIS 0 0 0 6 376\0\0 003 MSG 005 HELLO 377 207z=304fTL
343
- REDIS
- 0006是版本号
- 376是SELECTDB常量
- \0是db 0
- \0是字符串类型
- 003 MSG表示字符串MSG
- 005 HELLO 表示字符串HELLO
- 377是EOF
- 后面是md5
5.读入RDB文件
在Redis启动的时候,会自动加载RDB文件,加载成功后,服务器才处理客户端的请求。
十一、AOF持久化
不同于RDB一次存储整个数据库状态
AOF(Append Only File)是每次执行写命令,就append一条指令到文件。
1.命令追加
struct redisServer {
sds aof_buf;
}
在redis 服务器对象中,有一个aof_buf属性,用于存储AOF命令
每次服务器执行写命令的时候,都会往这个缓冲区写AOF命令。
在Redis的时间事件中,会调用flshAppendOnlyFile函数,决定是否吧缓冲区的数据flush到文件。
例如如果执行命令 set key value
就会产生下面的AOF命令:
*3\4\n$3\r\nSET\4\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
2.AOF文件载入
在启动REDIS的时候,会加载AOF文件
3.AOF重写
当写命令很多的时候,甚至都是操作很少的几个键的话,例如不断地修改一个key的值,这样就会有很多命令。
这时候就可以执行重写命令,来把AOF命令压缩。
例如命令:
set test a
set test b
set test c
会被压缩为
set test c
因为上面的两条命令就没意义了。
实际的重写进程,不会去扫描AOF文件,而是会扫描数据库的键值对,然后执行SET或者push等命令。
4.AOF后台重写
当执行BGREWRITEAOF命令,Redis就会执行后台重写
- 启动一个新的进程,把数据库状态复制过去,执行重写操作,也就是扫描所有数据库,里面的所有key
- 原进程继续处理客户端请求,写操作写入到缓冲区
- 新进程执行完重写操作后,生成新的AOF文件,发送信号给原进程
- 原进程收到信号后,把把缓冲区的命令append到新的AOF文件,然后当前的AOF文件切换为新的AOF文件
十二、事件
redis是两种事件
- 文件事件,就是处理客户端的请求
- 时间时间,处理redis自身的一些定时任务
1.文件事件
Redis使用IO多路复用的方式,当有客户端的请求(例如Socket有数据可以读取,有数据可以写),就会生成一个事件,塞到处理队列里面
每当Redis执行文件事件的时候,就从中取一个事件来执行。
事件包含
- Socket号
- 客户端实例
2.时间事件
redis里面会有一个时间事件队列
一个时间事件包含
- id 事件的id
- when 什么时候执行,时间精确到毫秒
- timeproc 执行函数
具体实现就是,redis会每个100毫秒,就去这个队列里面遍历,看哪个时间可以执行(when小于当前时间),如果可以就执行。
- 如果是定期事件,执行完,就会从队列里面删除
- 如果是周期性事件,执行完,删除事件后,会创建一个新的事件插入到队列
其实现在Redis只有一个事件事件,就是serverCron
这个事件会执行:
- 更新服务器的各类统计信息
- 清理数据库中过期的键值对
- 关闭和清理失效的客户端
- 尝试进行RDB和AOF的持久化操作
- 如果是主服务器,对从服务器进行定期同步
- 如果是集群模式,对集群进行定期同步和连接测试
serverCron每秒运行10次,也就是每100毫秒一次。
3. 事件的调度
当Redis服务器启动后,跑完了初始化的任务,就会死循环得跑下面这个流程:
def aeProcessEvents():
time_event=aeSearchNearestTimer() #寻找最近的时间事件
remaind_ms=time_event.when-unix_ts_now() #计算事件要在多少毫秒后执行
if remaind_ms<0:
remaind_ms=0
timeval=create_timeval_with_ms(remaind_ms) #通过remaind_ms计算最长阻塞时间
aeApiPoll(timeval) #等待文件事件,超时时间为最长阻塞时间,如果remaind_ms=0,就马上返回,不阻塞
processFileEvents() #执行文件事件
processTimeEvents() #执行时间事件
所以总的来说
- 会阻塞进程来等待文件事件
- 阻塞的时间不会超过下个时间事件的执行时间
这样可以保证
- 时间事件可以尽量准时(不是完全准时)地被执行
- 文件事件也可以及时处理
十三、客户端
Redis的服务端对象有一个列表,保存所有的客户端对象,每个连接过来的客户端,都会创建一个客户端实例。
struct redisServer {
list *clients;
}
redis命令 client list
可以查看所有连接的客户端。
客户端的属性有:
- flags,使用不同的标志来表示客户端的角色,例如是普通的客户端,还是主从同步的客户端等等
- 输入缓冲区,客户端发来的数据,首先放到这里,然后再进行处理
- argv,argc,命令的参数值,和数量
- 输出缓冲区,有两个,一个是固定大小的16KB,一个是可变大小的
- 套接字ID,
- 身份验证,记录客户端是否进行了身份验证,如果否,不能执行除auth外的其他命令
- 命令实现函数,命令名和实现函数的映射,这个应该是全局统一的,不是每个客户端都有一个的
- 名字,客户端可以自己设置名字,如果没有设置,为NULL
- 创建时间,客户端的创建时间戳
- 上一次执行命令的时间,用于计算空转时间
客户端的生命周期:
- 创建,当客户端连接上服务端后,服务端就会创建一个客户端实例,添加到clients列表的后面
- 客户端发送命令给服务端
3. 发送的命令首先存储到输入缓冲区,然后生成文件事件E1 - 服务端处理命令
4. 当Redis主进程执行该文件事件E1时,就会解析输入缓冲区,解析里面的参数,和参数个数,存储到argv和argc。例如如果执行命令set test aa
,参数就是['set','test','aa']
,个数是3
5. 解析后,根据argv[0],在命令实现函数里面寻找set命令对应的执行函数,命令部分大小写
6. 调用执行函数,传入client对象
7. 执行函数里面,执行相应的操作,把返回结果,存储到输出缓冲区。根据返回结果的大小,决定存储到哪个缓冲区,如果大小超出服务器的限制,就直接关闭客户端。
8. 生成套接字可写的文件事件E2
9. 结束当前的文件事件 - 服务端返回命令的结果
10. 当服务端执行文件事件E2时,把输出缓冲区的数据,传输给客户端 - 关闭客户端
12. 当出现以下情况,会关闭客户端
13. 客户端进程退出或者杀死,这样网络连接(Socket)就会被关闭,客户端也会被关闭
14. 客户端发送不符合协议格式的请求
15. 客户端成功CLIENT KILL命令的目标
16. 如果服务器设置了timeout属性(客户端的超时时间),而客户端的空转时间超过timeout。如果客户端在执行BLPOP,订阅等命令,就不会被关闭。
17. 发送的数据或者返回的数据超出缓冲区的限制
十四、服务器
1. 执行set命令的整个流程
参考上面的客户端生命周期
2.serverCron函数
参考时间事件里面的serverCron说明。
除此之外还有:
- 更新时间缓存,Redis里面有很多地方要用到服务器时间,对于时间精度要求不高的地方,Redis会使用时间缓存,而不是再去调用系统函数。
- 更新LRU时钟,
- 更新服务器每秒执行命令次数
- 更新服务器内存峰值
- 处理SIGTERM信号。启动服务器的时候,Redis会监听SIGTERM信号,当收到信息后,会把shutdown_asap属性置为1,在serverCront中,如果这个属性是1,就会关闭服务器
- 管理客户端资源
7. 如果客户端连接超时,关闭客户端 - 管理数据库资源,例如删除过期的键,对字典进行收缩操作
- 执行被延迟的BGREWRITEAOF操作
- 检查持久化操作的状态
- 将AOF缓冲区的内容写入AOF文件
- 增加cronloops计数器,这个计数器记录了serverCron被执行的次数
3.初始化服务器
当启动服务器的时候,服务器会做以下操作
- 初始化状态结构
- 载入配置选项
- 初始化服务器数据结构,例如服务器实例,例如共享内存的数据
- 还原数据库状态,即载入RDB文件或者AOF文件
- 执行事件循环,就是事件章节中的死循环