共读《redis设计与实现》-单机(一)
上一章我们讲了 redis 基本类型的数据结构
和 对象系统
,这篇来说一下单机redis 的知识点。
一、数据库
一个数据库在redis中就有一个结构体,而数据库的结构体是由redisServer这个结构体持有。
也就是redis服务器对应一个redisService 结构体,一个redisServer结构体持有多个redisDB数组,并且存储了数组的大小。
struct redisServer {
...
// 一个数组保存服务器所有的数据库
redisDb *db;
// 服务器的数据库的数量//初始默认为16
int dbnum;
// 过期字典// 保存键的过期时间
dict *expires;
// 记录rdb 保存条件的数组
struct saveparam *saveparam;
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
// aof 缓冲区
sds aof_buf;
// 一个链表保存了所有客户端的状态
list *client;
...
}
二、客户端
然后我们再说一下客户端,每个客户端是也是有一个redisClient结构体
typedef struct redisClient {
. . .
// 客户端的名称,使用Client setname 命令设置
rojb *name;
// 记录客户端正在使用的数据库
redisDb *db;
// 套接字描述符号,伪客户端为 -1;客户端为>-1的整数
int fd;
// 记录了客户端的角色,可以是单个值 也可以多个值
int flags;
// 客户端输入缓冲区,用来保存客户端输入的请求//不能超过1G,否则会关闭
sds querbuf;
// 客户端发送服务端请求之后,
//服务端解析请求,将命令参数保存在客户端 argv 中,命令个数 保存在argc
robj **argv;
int argc;
. . .
}
客户端的结构体中db 的指针指向 当前正在使用的数据库的地址。
所以当我们 使用 SELECT 命令切换数据库的时候就是将 redisClient 的db 指针切换了一个位置
注意点
三、键空间
我们之前看字典的时候已经讲过,每个数据库其实就是一个字典
,我们平常存储的数据在数据库这个字典中,key是字典的key,value 是字典的value。(其实每个key 就是一个SDS结构,所以字典的key 是一个SDS 结构的存储体,value 可能是SDS 可能是 字典、序列等其他基本结构体
)
3.1 添加/更新/删除
其实键的 添加/更新/删除 就是在字典中 添加key-value 键值对 和 更新 删除 键值对的动作。
3.2 键的生存时间/过期时间
我们可以给键 设置一个时间 ,当创建之后过多久就失效
为 生存时间
;当到达某个时间点就失效
是 过期时间
3.2.1 SETEX/SEPIRE/PEXPIRE/EXPIREAT/PEXPIREAT
键盘的过期时间,我们可以从redisServer 的结构体中可以看出,其实就是对每个键``存储
了一个过期时间
。
Redis 有四个
不同的命令可以用于设置键的生存时间(键可以存在名久)或过期时间
(键什么时候会被删除):
EXPIRE
<key>sttl>命令用于将键key 的生存时间设置为tt1秒PEXPIRE
<key><tl>命令用于将键key 的生存时间设置为 tt1毫秒。BXPIREAT
Stimestamp>命令用于将键 key 的过期时间设置为timestamp所指定的秒数时间戳。 PEXPIREAT
<key> <timestamp>命令用于将键key 的过期时间设置为 timestamp所指定的亳秒数时间戳。
虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAI
三个命令都是使用 PEXPIREAT 命令来实现的:无论客户端执行的是以上四个命令中的哪-
个,经过转换
之后,最终
的执行效果都和执行PEXPIREAT
命令一样
所以最后干活的是PEXPIREAT ,其他的就是对于不同业务下的衍生api 而已。
关于TTL/PTTL 命令
3.2.2 过期键的删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
三种策略优缺点
定时删除 能够及时释放 内存空间,但是如果遇到大量对键 过期,那么会占用很大对cpu 资源
惰性删除 能够解决cpu 资源对问题,但是 会浪费大量对存储空间,有 内存泄漏 的风险
定期删除 相当于平衡 前两种的优缺点。
一般我们将惰性删除
和定期删除``配合
使用
具体使用:
惰性:所有读写库的redis 数据库执行 命令之前 都会调用expireIfveeded 函数检查过期时间,如果过期,那么就删除
定期:redis 周期性
函数Servercron
执行 的时候 会调用activeExpireCycle
函数,在 规定时间
内 遍历一次数据库,随机
的访问一些键 查看过期时间,过期删除
。
3.3 RDB/AOF 持久化时 过期键处理
RDB
生成:
对于RDB 快照类型的,如果是过期了,那么下一次生成快照的时候就不会记录在RDB文件中
载入:
如果是主服务器
加载RDB
文件,那么会对键的过期时间进行检查
,如果是过期了(在生成RDB文件和加载RDB文件之间的时间段内过期),那就不会被加载。
如果是从服务器
加载RDB
文件,那么不会
检查过期键,全部加载
。(但是和主服务器``同步
的时候,从服务器的数据库都会被清空
)
AOF
生成:
只要还没有被删除,那么不会对AOF产生影响,AOF会全部记录,如果执行期间被删除 会增加一条DELE命令
加载:
如果是过期了,那么不会被加载
3.4 复制期间过期间处理
主服务器 删除的时候 会向 从服务器 发送删除命令
从服务器 遇到过期键 不会处理,和平常的键一样,即使客户端查询也会返回。
我们知道 redis 是一个内存数据库,也就是所有的数据都在内存中,cpu 取数据的时候直接从内存中查找,不用在调用系统io 从磁盘加载数据了。这也就是为何redis 比其他 存储磁盘的数据库快的原因。
但是在内存中的数据有个极大的缺点:如果服务器一旦关闭,那么数据就不在了,因为内存的数据并没有写到磁盘上,所以redis 需要提供一个能够写入磁盘的机制。
redis 提供了两种持久化机制:rdb持久化 aof持久化
键空间的维护操作
当redis 命令对于数据库的读写时,服务器不仅会对键空间进行客户端的请求命令,还会执行一些额外的操作
- 命中率:读取一个键之后,服务器会依据进键是否存在来更新 键空间命中率 和 不命中率
- 闲置时间:就是键多久没有访问过了,等到命中这个键时会更新这个值。
- 过期键
- watch 命令:使用warch 命令监控一个键,那么键修改时会将这个键置成 脏
- 脏数据:服务器每修改一个键,会对 脏键 计数器的值+1;
- 数据库通知功能:如果开启这个功能,那么修改redis 会通知数据库
四、RDB 持久化
对于RDB 持久化,我们可以认为就是一个内存的快照,也就是将某一瞬间redis 的数据给存储下来。
使用这个持久化有两种命令:SAVE / BGSAVE
SAVE
这个命令持久化的时候,redis 处于阻塞状态,也就是redis 不接受客户端发过来的任何请求,全力的去处理这个请求。
BGSAVE
对于save 来说,我们要是因为持久化导致redis 不能使用,这个显然会有问题。因为如果数据量特别多,那么我们为了持久化,消耗的时间也就很多,业务阻塞了。
和save 不同,为了能够使得持久化同时也能运行客户端请求,redis 对于bgsave 分出一个线程去处理 持久化,这样就不是阻塞的了。
save 和 bgsave 不能够同时执行,考虑防止竞争问题、同时操作io的效率问题。
载入
因为RDB存储的是redis 的快照,所以redis 没有载入 rdb文件的命令,程序启动的时候会自动加载rdb文件。
另外一点需要注意的是:因为aof文件比rdb 更新的更频繁,也就是数据更新,那么存在aof文件,就会加载aof文件而不会加载rdb文件了,可以使用配置将aof文件读取关闭
自动保存条件
redis 对于bgsave 可以设置每隔多久进行一次rdb 保存的,可以通过启动是save 参数进行设置。
我们可以从redisServer 的结构中看出,这个自动保存的条件其实是存储起来的,也就是redisServer持有这个自动存储条件,并在规定条件下进行一次调用BGSAVE命令
dirty 计数器 和lastsave 属性
服务器在每执行一次操作都会更新一次dirty计数器,比如dirty 计数器为123,那么说明距离上次保存,服务器执行了123次命令。
服务器 之所以可以 自动保存
,是因为 时间事件
不断的去扫描 redisServer
然后看 saveparams 属性
是否满足自动保存。然后在调用bgsave
serverCrom 函数会 遍历 saveparams ,看其中的条件是不是被满足了
RDB文件结构
rdb文件以二进制形式存储,我们可以通过 od 命令来解析 rdb文件
文件结构
说明:
我们用全大写表示 常量标识;使用全小写标识 变量
- REDIS:常量标识符(5字节)
- db_version:版本号(4字节)
- 数据库:也就是存储的具体数据,具体长度由保存的数据来说明,如果没有数据,那就没有这个字段
- EOF:结束标识位(1字节),也就是如果读到这个地方表示 rdb文件正文
读取
完毕 - check_sum:校验位置,就是前面的数字长度;
SELECTDB:常量(1字节),标识为这里是数据库
db_number:数据库号码(1-5字节不等)
key_value_pairs:数据库中具体存储的值
rdb文件中 数据库中 数据的值(k-v结构)
EXPIRETIME_MS:常量,标识带有过期键
过期键的时间
KEYTYPE:上图写的是REDIS_RDB_TYPE_SET,这个是 存储的类型,方便读取 value 的值
key: 存储的key
value :存储的value 可能是 SDS、HASH 之类的。
五、AOF 持久化
AOF 持久化功能的实现可以分为命令追加(append )
、文件写人
、文件同步 (sync)``三个
步骤。
命令追加
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到redisServer 结构体中 的 aof buf
缓冲区
的末尾
如果在来一条命令,那么在向缓冲区末尾添加。
Redis 的服务器进程就是一个事件循环
(1oop),这个循环中的文件事件
负责接收客户端
的命令请求
,以及向客户端发送命令回复,而时间事件
则负责执行像servercron 函数这样需要定时运行
的函数。---这段看不懂就略过,其实是 下面要说 的事件
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 f1ushAppendonlyFile函数,考虑是否需要将 aof buf 缓冲区中的内容写人和保存到 AOF 文件里面。
载入/数据还原
创建一个不带网络连接
的伪客户端
(fake client )是因为 Redis 的命令只能
在客户端上下文
中执行
,而载人 AOF 文件时所使用的命令直接来源于 AOF 文件
而不是网络连接,所以服务器使用了一个没有网络连接
的伪客户
,和客户端效果是一样的。
AOF 重写
如果我们redis 运行的事件长了,那么就会使得aof 文件变得很大,而且这个文件中很多命令是浪费空间的,比如 push key v1;push key v2... 所以,redis 对aof 文件进行了重写,让这些命令合并为一条命令,减少aof 的空间
重写原理:
aof 重写 不需要
进行 读取/写入 原 aof 文件,也就是 完全 不操作原文件
他主要是看数据库中
数据的状态
,使用命令将 数据库中的数据 写入文件
比如:
数据库中有个 numbers:one,two,three
这样的结构,之前是
push numbers one;
push numbers two;
push numbers three;
三个命令
我们直接读取数据库,我们不清楚过程,所以我们将之前的三个命令变成一个:
push numbers one two three
这样就是压缩了
之前重写aof 文件的时候都是 不接受新的命令,为了不影响 使用,所以使用了后台重写 命令。
后台重写,为了保证数据的一致性,使用了aof 重写缓冲区。
文件写入和同步
六、事件
Redis 服务器是一个事件驱动程序
,服务器需要处理以下两类
事件:
文件事件
(file event ):Redis服务器
通过套接字
(含义就是通过网络链接)与客户端(或者其他 Redis 服务器)进行连接,而文件事件
就是服务器
对套接字
操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。时间事件
(time event ):Redis 服务器中的一些操作(比如servercron 两数)需要在给定
的时间点
执行,而时间事件
就是服务器对这类定时操作的抽象。
文本事件
Redis 基于 Reactor模式
开发了自己的网络事件处理器
:这个处理器被称为文件事件处理器
(file event handler):
- 文件事件处理器使用
I/0 多路复用
(multiplexing)【https://www.cnblogs.com/zhangxiaoji/p/16152141.html】程序来同时监听多个套接宇,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写人(write入关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程方式运行,但通过使用 IO 多路复用程序来监听多个套接宇,文件事件处理器既实现了高性能的网络通信模型(可以理解为 一个服务端使用了一个线程(或者少量的线程)来处理多个客户端请求),又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性
注意:io 多路复用和 文本事件分派器 中间的队列是 单个的,也就是文件事件分派器一次只处理一个事件。
时间事件
我们从之前的讲述中可以看到,和时间相关的都是由 redisCron 函数进行处理的,那么它就是我们的 时间事件应用实例了。
Redis 的时间事件分为以下两类:
- 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序× 在当前时间
的 30毫秒之后执行一次。 - 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔 30毫秒就执行-
一次。
个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的全局唯一四D(标识号)。1D号按从小到大的顺序递增,
新事件的1D 号比旧事件的1D 号要大。 - when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间。
- timeProc:时间事件处理器,一个两数。当时间事件到达时,服务器就会调用相
应的处理器来处理事件。
事件的调度与执行
因为redis 存在两种事件类型,所以 redis 必须有个调度器去解决何时处理 文本事件 何时 处理 时间事件
这个是 aeProcessEvents函数来进行的
后面对 服务器 和 客户端 在进行详细的研究。
参考资料#
《Redis设计与实现》-黄健宏
部分图片来与百度搜索