《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 可以查看所有连接的客户端。

客户端的属性有:

  1. flags,使用不同的标志来表示客户端的角色,例如是普通的客户端,还是主从同步的客户端等等
  2. 输入缓冲区,客户端发来的数据,首先放到这里,然后再进行处理
  3. argv,argc,命令的参数值,和数量
  4. 输出缓冲区,有两个,一个是固定大小的16KB,一个是可变大小的
  5. 套接字ID,
  6. 身份验证,记录客户端是否进行了身份验证,如果否,不能执行除auth外的其他命令
  7. 命令实现函数,命令名和实现函数的映射,这个应该是全局统一的,不是每个客户端都有一个的
  8. 名字,客户端可以自己设置名字,如果没有设置,为NULL
  9. 创建时间,客户端的创建时间戳
  10. 上一次执行命令的时间,用于计算空转时间

客户端的生命周期:

  1. 创建,当客户端连接上服务端后,服务端就会创建一个客户端实例,添加到clients列表的后面
  2. 客户端发送命令给服务端
    3. 发送的命令首先存储到输入缓冲区,然后生成文件事件E1
  3. 服务端处理命令
    4. 当Redis主进程执行该文件事件E1时,就会解析输入缓冲区,解析里面的参数,和参数个数,存储到argv和argc。例如如果执行命令 set test aa,参数就是['set','test','aa'],个数是3
    5. 解析后,根据argv[0],在命令实现函数里面寻找set命令对应的执行函数,命令部分大小写
    6. 调用执行函数,传入client对象
    7. 执行函数里面,执行相应的操作,把返回结果,存储到输出缓冲区。根据返回结果的大小,决定存储到哪个缓冲区,如果大小超出服务器的限制,就直接关闭客户端。
    8. 生成套接字可写的文件事件E2
    9. 结束当前的文件事件
  4. 服务端返回命令的结果
    10. 当服务端执行文件事件E2时,把输出缓冲区的数据,传输给客户端
  5. 关闭客户端
    12. 当出现以下情况,会关闭客户端
    13. 客户端进程退出或者杀死,这样网络连接(Socket)就会被关闭,客户端也会被关闭
    14. 客户端发送不符合协议格式的请求
    15. 客户端成功CLIENT KILL命令的目标
    16. 如果服务器设置了timeout属性(客户端的超时时间),而客户端的空转时间超过timeout。如果客户端在执行BLPOP,订阅等命令,就不会被关闭。
    17. 发送的数据或者返回的数据超出缓冲区的限制

十四、服务器

1. 执行set命令的整个流程

参考上面的客户端生命周期

2.serverCron函数

参考时间事件里面的serverCron说明。
除此之外还有:

  1. 更新时间缓存,Redis里面有很多地方要用到服务器时间,对于时间精度要求不高的地方,Redis会使用时间缓存,而不是再去调用系统函数。
  2. 更新LRU时钟,
  3. 更新服务器每秒执行命令次数
  4. 更新服务器内存峰值
  5. 处理SIGTERM信号。启动服务器的时候,Redis会监听SIGTERM信号,当收到信息后,会把shutdown_asap属性置为1,在serverCront中,如果这个属性是1,就会关闭服务器
  6. 管理客户端资源
    7. 如果客户端连接超时,关闭客户端
  7. 管理数据库资源,例如删除过期的键,对字典进行收缩操作
  8. 执行被延迟的BGREWRITEAOF操作
  9. 检查持久化操作的状态
  10. 将AOF缓冲区的内容写入AOF文件
  11. 增加cronloops计数器,这个计数器记录了serverCron被执行的次数

3.初始化服务器

当启动服务器的时候,服务器会做以下操作

  1. 初始化状态结构
  2. 载入配置选项
  3. 初始化服务器数据结构,例如服务器实例,例如共享内存的数据
  4. 还原数据库状态,即载入RDB文件或者AOF文件
  5. 执行事件循环,就是事件章节中的死循环
posted @ 2019-12-23 17:53  Xjng  阅读(730)  评论(0编辑  收藏  举报