《Redis设计与实现》第一版 学习记录
Redis
Redis ,一个用C写的基于内存的键值数据库软件。与传统的关系型数据库产品最大的不同是,Redis基于内存,读写速度快(尤其是读,磁盘的顺序写倒也不慢)。
1 数据结构与对象
redis 的键(key)只有1种数据类型 string,而值(value)有5种数据类型
- string 字符串
- list 列表
- hash 哈希
- set 集合
- zset 有序集 1
1.1 string字符串类型
string 有两种实现:
1.1.1 sds
当值是字符串类型时,redis使用sds类型来存储。sds (Simple Dynamic String) 简单动态字符串。redis很少直接使用C的char*来表示字符串,而是使用sds——redis的一种字符串实现——来取代char*。
struct sdshdr{
// 当前字符串占用的长度
int len
// 分配给sdshdr的总长度-字符串占用的铲毒=剩余空间
int free
// 字符串的内容
char* buf
// buf为"Hello World\0" '/0'为结束符,所以len(buf)=len+1
}
从sdshdr的实现就能看出,sdshdr比char*,能高效地支持一些Redis常用操作,比如append追加、长度计算等。
append时,先判断所需长度(即新追加的字符串长度)。如果所需长度>free,即分配给当前sdshdr的长度不足以容纳整个追加后字符串的长度,就进行扩容,为buf分配两倍于所需总空间。(比如追加后的字符串为"Hello World",所需空间为12,则扩容后的len为11,free为12)
长度计算时,直接获取该sdshdr的len成员(len的值随buf变化而动态更新)。
sds代替char*,还能保证二进制安全。即不妄图以某种特殊格式解析数据,如C中的strlen()依赖于字符串的’/0’来判断字符串的结束。
1.1.2 long
当val是int类型时,redis使用long类型来存储。
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
使用TOC
语法
1.2 list 列表类型
list列表类型有两种实现
1.2.1 ziplist 压缩列表
Ziplist 是由一系列特殊编码的内存块构成的列表,一个ziplist可以包含多个节点(entry),每个节点可以保存一个长度受限的字符数组或整数。
因为ziplist节约内存的特点,因此redis值的5种类型中,有3种(list、hash、zset)都采用了ziplist作为该类型初始化
(随着保存的元素的增多,满足一定条件时,转换为其他实现方式)时的底层实现。
1.2.2 linkedlist 双端链表
1.3 hash 哈希类型
hash哈希类型有两种实现
1.3.1 ziplist 压缩列表
Ziplist 是由一系列特殊编码的内存块构成的列表,一个ziplist可以包含多个节点(entry),每个节点可以保存一个长度受限的字符数组或整数。
因为ziplist节约内存的特点,因此redis值的5种类型中,有3种(list、hash、zset)都采用了ziplist作为该类型初始化
(随着保存的元素的增多,满足一定条件时,转换为其他实现方式)时的底层实现。
1.3.2 hashtable 字典
1.4 set 集合类型
set集合类型有两种实现
1.4.1 intset 整数集合
intset用于有序、无重复地保存多个整数值。
如果一个set集合1.只保存整数元素2.元素的数量不多,那这个set采用intset作为底层实现
1.4.2 hashtable 字典
1.5 zset 有序集类型
zset有序集类型有两种实现
1.5.1 ziplist 压缩列表
1.5.2 skiplist 跳跃表
2 功能
除了针对单个键值对的操作外, Redis 还提供了一些同时对多个键值对进行处理的功能, 比如事务和 Lua 脚本。
另外, 一些辅助性的功能, 比如慢查询, 以及一些和数据库无关的功能, 比如订阅与发布, 我们也会经常用到。
通过理解这些功能的底层实现, 我们可以更有效地使用它们。
这一部分将对这些功能进行介绍。
2.1 事务
2.2 订阅与发布
2.3 lua 脚本
2.4 排序
2.5 二进制位数组
2.6 慢查询日志
2.7 监视器
3 内部运作机制(单机)
3.1 持久化
redis持久化分为两种,rdb和aof
3.1.1 rdb
rdb将数据库的当前状态(快照snapshot)以磁盘文件的形式保存。在redis重新启动时,可通过载入rdb文件来还愿数据库额度状态
执行rdb的函数:rdbSave()函数
有两条命令可以触发rdbSave:
- SAVE命令:会阻塞主进程,直到rdbSave完成。(dbSave期间,服务端不能处理客户端的任何请求)
- BGSAVE命令:主进程会fork出一个子进程来做rdbSave,rdbSave完成后,子进程发送信号通知主进程(dbSave期间,服务端正常处理客户端的请求)
SAVE和BGSAVE不可同时执行,也不能多个SAVE或多个BGSAVE同时执行。
redis服务器启动时,通过rdbLoad()函数载入rdb文件。
rdb载入过程中,每载入1000个键,处理1次期间到达的请求,且只处理“发布与订阅”相关的请求。(“发布与订阅”和其他数据库功能完全隔离)
3.1.2 aof
aof(Append Only File)使用redis的网络通讯协议的形式(理解为某种编码),将所有对数据库进行过的写入
相关的命令(及其参数)记录到aof文件中,以此记录数据库状态。
aof追加过程:
- 将命令传播给aof程序
- aof程序将命令转换为网络通讯协议的格式,追加到aof缓存
- 按
保存模式
写磁盘(WRITE),将aof缓存中的内容追加到aof文件末尾(SAVE)
aof保存模式:
- 不保存:只在redis关闭、aof功能关闭、aof缓存刷新时,进行保存。
- 每秒保存一次
- 每执行一个命令保存一次
用aof文件还原数据
:
- 创建不带网络连接的伪客户端(fake client)
- 读取aof文件,将文本内容转换为(写入)命令
- 伪客户端执行命令
- 循环2和3,直到aof文件中所有命令被执行
aof文件不断地追加内容,难道让追加不受限制吗?
aof文件重写
(rewrite):用一个新aof文件代替旧aof文件,两者保存的数据库状态一致,但新aof文件体积更小。
aof文件重写的原理:新aof仅描述数据的当前(最后)状态(而不记录之前的变化过程)
aof重写放在(后台)子进程里执行
aof重写的过程中,主进程对外服务,若数据库状态发生变化,产生aof开始时和结束时两个时间点数据不一致的问题,如何解决?
主进程将后台aof重写过程中,发生过的写命令,保存在一个aof重写缓存中。等aof重写完成后,再将缓存内容追加到新aof文件末尾
aof重写的触发条件:
手动:BGREWRITEAOF命令
自动:满足以下所有条件
- 没有 BGSAVE 命令在进行。
- 没有 BGREWRITEAOF 在进行。
- 当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。
- 当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比(默认值为100%)。
3.2 事件
事件是redis服务器的核心,它处理着2项重要的任务
- 处理文件事件:在多个客户端中实现多路复用,处理它们发来的命令请求,并将命令的执行结果返回给客户端
- 时间事件:实现服务器常规操作(server cron job)
3.2.1 文件事件
文件事件有2种,A_R读事件和A_W写事件。
3.2.1.1 A_R读事件
A_R读事件标志着客户端的命令请求
的发送状态
。
当一个客户端和服务端建立连接后,服务端为客户端绑定A_R读事件
。直到客户端断开连接,这个读事件才会被移除
。此时客户端还没向服务端发送命令,A_R属于等待状态
。
当客户端给服务端发送命令请求,且该请求已经到达时(相应的socket可以无阻塞地执行读操作)时,该客户端的读事件属于就绪状态
。
当事件处理器被执行时,就绪的读事件就会被识别,由文件事件分流器分给相应的事件处理器,进行处理。
3.2.1.2 A_W写事件
A_W写事件标志着客户端对命令结果
的接收状态
。
和客户端自始至终都关联着的读事件不同,只有在服务端有命令结果传回给客户端时,服务端才会为客户端关联
写事件。在命令结果传递结束后,服务端会解除
与客户端关联的写事件。
当客户端发送了命令请求,命令请求被接收并执行后,服务端需要将保存在缓存内的命令执行结果返回给客户端,此时服务端为客户端关联
写事件。
当服务器有命令结果需要返回给客户端,但客户端的socket还未能执行无阻塞写,那么写事件处于等待状态
。
当服务器有命令结果需要返回给客户端,并且客户端可以执行无阻塞写,那么写事件处于就绪状态
。
当命令执行结果被传回给客户端后,客户端和写事件之间的关联被解除
(只剩下读事件在关联),至此,返回命令执行结果的动作执行完毕。
值得注意的是,当客户端关联写事件时,此时客户端同时关联了两种文件事件——读事件和写事件——写事件是在客户端建立连接时就关联上的。
由于在同一次文件事件处理器的调用中, 单个客户端只能执行其中一种事件(要么读,要么写,但不能又读又写),所以当客户端同时关联了读事件和写事件,并且读事件和写事件同时处于就绪状态时,事件处理器优先处理读事件。也就是既有命令执行结果要返回,又有新命令请求传来时,服务器优先处理新命令请求。
3.2.2 时间事件
时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表保存在服务器状态中。
每个时间事件主要有3个属性:
- when 记录了应该在什么时间点执行事件处理函数,以毫秒格式的 UNIX 时间戳为单位
- timeProc 事件处理函数。事件处理函数的是否返回ae.h/AE_NOMORE,决定了该事件是
单次
执行还是循环
执行。单次执行的时间事件会在执行完后从无序链表中被删除;循环执行的时间事件会在执行完后更新when的值为下一次执行的时间点。 - next 指针
当时间事件处理器被执行时,会遍历无序链表中的所有节点,检查它们的到达时间(when属性),并执行其中已到达的事件。
正常模式的Redis中只带有serverCron
一个时间事件(实际上该函数中可以包括n件事),在 benchmark 模式下, Redis 也只使用两个时间事件。因此无序链表就是一个指针(只有一个时间事件,链表只有1个节点),遍历不会影响时间事件处理器的性能。
serverCron
:对于持续运行的服务器来说,服务器需要定期对自身的资源和状态进行必要的检查和整理,从而让服务器维持在一个健康稳定的状态。这类操作被称为常规操作
(cron job)
在 Redis 中, 常规操作由 redis.c/serverCron 实现, 它主要执行以下操作:
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
- 清理数据库中的过期键值对。
- 对不合理的数据库进行大小调整。
- 关闭和清理连接失效的客户端。
- 尝试进行 AOF 或 RDB 持久化操作。
- 如果服务器是主节点的话,对附属节点进行定期同步。
- 如果处于集群模式的话,对集群进行定期同步和连接测试。
redis将serverCron作为时间事件来运行,以保证它会每隔一段时间就自动运行一次。又因为serverCron需要一直被执行,所以它是一个循环
时间事件。
Redis2.6中,默认每100ms执行一次serverCron,1s执行10次,可在redis.conf中进行修改。
3.2.3 事件的执行和调度
既然 Redis 里面既有文件事件, 又有时间事件, 那么如何调度这两种事件就成了一个关键问题。
简单地说, Redis 里面的两种事件呈合作关系, 它们之间包含以下三种属性:
- 一种事件会等待另一种事件执行完毕之后,才开始执行,事件之间
不会出现抢占
。- 事件处理器
先
处理文件事件(处理命令请求),再
执行时间事件(调用 serverCron)- 文件事件的等待时间(类 poll 函数的最大阻塞时间),由距离到达时间最短的时间事件决定。
这些属性表明, 实际
处理时间事件
的时间, 通常会比时间事件所预定的时间要晚
, 至于延迟的时间有多长, 取决于时间事件执行之前, 执行文件事件所消耗的时间。(根据情况, 如果处理文件事件耗费了非常多的时间, serverCron 被推迟到一两秒之后才能执行, 也是有可能的)
3.3 服务端和客户端
3.3.1 初始化服务端
从启动 Redis 服务器, 到服务器可以接受外来客户端的网络连接这段时间, Redis 需要执行一系列初始化操作。
整个初始化过程可以分为以下六个步骤:
- 初始化服务器全局状态。
- 载入配置文件。
- 创建 daemon 进程。
- 初始化服务器功能模块。
- 载入数据。
- 开始事件循环。
3.3.2 客户端连接到服务器
当 redis 服务端初始化完成后,它就准备好接收外来的客户端的连接了。
当一个客户端通过套接字函数 connect 到服务器时,服务器执行以下步骤
- 服务器通过文件事件无阻塞地 accept 客户端连接,并返回一个套接字描述符 fd。
- 服务器创建一个客户端对应的 redis.h/redisClient 结构实例,并将该实例加到服务端的已连接客户端的链表中。
- 服务器在事件处理器为该 fd 关联读文件事件。
此时服务端可以接收客户端发来的命令请求了。
Redis 以多路复用的方式来处理多个客户端, 为了让多个客户端之间独立分开、不互相干扰, 服务器为每个已连接客户端维持一个 redisClient 结构, 从而单独保存该客户端的状态信息。
redisClient 结构主要包含以下信息:
·套接字描述符。
·客户端正在使用的数据库指针和数据库号码。
·客户端的查询缓冲(query buffer)和回复缓存(reply buffer)。
·一个指向命令函数的指针,以及字符串形式的命令、命令参数和命令个数,这些属性会在命令执行时使用。
·客户端状态:记录了客户端是否处于 SLAVE 、 MONITOR 或者事务状态。
·实现事务功能(比如 MULTI 和 WATCH)所需的数据结构。
·实现阻塞功能(比如 BLPOP 和 BRPOPLPUSH)所需的数据结构。
·实现订阅与发布功能(比如 PUBLISH 和 SUBSCRIBE)所需的数据结构。
·统计数据和选项:客户端创建的时间,客户端和服务器最后交互的时间,缓存的大小,等等。
3.3.3 命令的请求、处理和结果返回
当客户端连上服务器之后, 客户端就可以向服务器发送命令请求了。
从客户端发送命令请求, 到命令被服务器处理、并将结果返回客户端, 整个过程有以下步骤:
- 客户端通过套接字向服务器传送命令协议数据。
- 服务器通过读事件来处理传入数据,并将数据保存在客户端对应 redisClient 结构的查询缓存中。
- 根据客户端查询缓存中的内容,程序从命令表中查找相应命令的实现函数。
- 程序执行命令的实现函数,修改服务器的全局状态 server 变量,并将命令的执行结果保存到客户端 redisClient 结构的回复缓存中,然后为该客户端的 fd 关联写事件。
- 当客户端 fd 的写事件就绪时,将回复缓存中的命令结果传回给客户端。至此,命令执行完毕。
3.4 数据库
本节描述了 Redis 数据库的构造和实现。
除了说明数据库是如何储存数据对象之外,本节还会讨论键的过期信息是如何保存,而 Redis 又是如何删除过期键的。
4 内部运作机制(多机)
4.1 复制
4.1 Sentinel
4.1 集群
完
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
居中的图片:
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎