上一章我们讲了 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 可能是 字典、序列等其他基本结构体)
其实键的 添加/更新/删除 就是在字典中 添加key-value 键值对 和 更新 删除 键值对的动作。
我们可以给键 设置一个时间 ,当创建之后过多久就失效为 生存时间;当到达某个时间点就失效 是 过期时间
键盘的过期时间,我们可以从redisServer 的结构体中可以看出,其实就是对每个键``存储了一个过期时间。
Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在名久)或过期时间
(键什么时候会被删除):
EXPIRE <key>sttl>命令用于将键key 的生存时间设置为tt1秒PEXPIRE <key><tl>命令用于将键key 的生存时间设置为 tt1毫秒。BXPIREAT PEXPIREAT <key> <timestamp>命令用于将键key 的过期时间设置为 timestamp所指定的亳秒数时间戳。虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAI
三个命令都是使用 PEXPIREAT 命令来实现的:无论客户端执行的是以上四个命令中的哪-
个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样
所以最后干活的是PEXPIREAT ,其他的就是对于不同业务下的衍生api 而已。
关于TTL/PTTL 命令

在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
定时删除 能够及时释放 内存空间,但是如果遇到大量对键 过期,那么会占用很大对cpu 资源
惰性删除 能够解决cpu 资源对问题,但是 会浪费大量对存储空间,有 内存泄漏 的风险
定期删除 相当于平衡 前两种的优缺点。
一般我们将惰性删除和定期删除``配合使用
具体使用:
惰性:所有读写库的redis 数据库执行 命令之前 都会调用expireIfveeded 函数检查过期时间,如果过期,那么就删除
定期:redis 周期性 函数Servercron执行 的时候 会调用activeExpireCycle函数,在 规定时间内 遍历一次数据库,随机的访问一些键 查看过期时间,过期删除。
生成:
对于RDB 快照类型的,如果是过期了,那么下一次生成快照的时候就不会记录在RDB文件中
载入:
如果是主服务器 加载RDB文件,那么会对键的过期时间进行检查,如果是过期了(在生成RDB文件和加载RDB文件之间的时间段内过期),那就不会被加载。
如果是从服务器 加载RDB 文件,那么不会检查过期键,全部加载。(但是和主服务器``同步的时候,从服务器的数据库都会被清空)
生成:
只要还没有被删除,那么不会对AOF产生影响,AOF会全部记录,如果执行期间被删除 会增加一条DELE命令
加载:
如果是过期了,那么不会被加载
主服务器 删除的时候 会向 从服务器 发送删除命令
从服务器 遇到过期键 不会处理,和平常的键一样,即使客户端查询也会返回。

我们知道 redis 是一个内存数据库,也就是所有的数据都在内存中,cpu 取数据的时候直接从内存中查找,不用在调用系统io 从磁盘加载数据了。这也就是为何redis 比其他 存储磁盘的数据库快的原因。
但是在内存中的数据有个极大的缺点:如果服务器一旦关闭,那么数据就不在了,因为内存的数据并没有写到磁盘上,所以redis 需要提供一个能够写入磁盘的机制。
redis 提供了两种持久化机制:rdb持久化 aof持久化
当redis 命令对于数据库的读写时,服务器不仅会对键空间进行客户端的请求命令,还会执行一些额外的操作
对于RDB 持久化,我们可以认为就是一个内存的快照,也就是将某一瞬间redis 的数据给存储下来。
使用这个持久化有两种命令:SAVE / BGSAVE
这个命令持久化的时候,redis 处于阻塞状态,也就是redis 不接受客户端发过来的任何请求,全力的去处理这个请求。
对于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计数器,比如dirty 计数器为123,那么说明距离上次保存,服务器执行了123次命令。
服务器 之所以可以 自动保存,是因为 时间事件 不断的去扫描 redisServer 然后看 saveparams 属性 是否满足自动保存。然后在调用bgsave

serverCrom 函数会 遍历 saveparams ,看其中的条件是不是被满足了
rdb文件以二进制形式存储,我们可以通过 od 命令来解析 rdb文件

说明:
我们用全大写表示 常量标识;使用全小写标识 变量
读取完毕
SELECTDB:常量(1字节),标识为这里是数据库
db_number:数据库号码(1-5字节不等)
key_value_pairs:数据库中具体存储的值

EXPIRETIME_MS:常量,标识带有过期键
过期键的时间
KEYTYPE:上图写的是REDIS_RDB_TYPE_SET,这个是 存储的类型,方便读取 value 的值
key: 存储的key
value :存储的value 可能是 SDS、HASH 之类的。

AOF 持久化功能的实现可以分为命令追加(append )、文件写人、文件同步 (sync)``三个步骤。
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到redisServer 结构体中 的 aof buf 缓冲区的末尾如果在来一条命令,那么在向缓冲区末尾添加。
Redis 的服务器进程就是一个事件循环(1oop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像servercron 函数这样需要定时运行的函数。---这段看不懂就略过,其实是 下面要说 的事件
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 f1ushAppendonlyFile函数,考虑是否需要将 aof buf 缓冲区中的内容写人和保存到 AOF 文件里面。



创建一个不带网络连接的伪客户端(fake client )是因为 Redis 的命令只能在客户端上下文中执行,而载人 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】程序来同时监听多个套接宇,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

注意:io 多路复用和 文本事件分派器 中间的队列是 单个的,也就是文件事件分派器一次只处理一个事件。

我们从之前的讲述中可以看到,和时间相关的都是由 redisCron 函数进行处理的,那么它就是我们的 时间事件应用实例了。
Redis 的时间事件分为以下两类:
个时间事件主要由以下三个属性组成:
因为redis 存在两种事件类型,所以 redis 必须有个调度器去解决何时处理 文本事件 何时 处理 时间事件
这个是 aeProcessEvents函数来进行的
后面对 服务器 和 客户端 在进行详细的研究。
参考资料#
《Redis设计与实现》-黄健宏
部分图片来与百度搜索