时间:2021-07-01 10:21:17 帮助过:30人阅读
《Redis设计与实现》读书笔记 很喜欢这本书的创作过程,以开源的方式,托管到Git上进行创作; 作者通读了Redis源码,并分享了详细的带注释的源码,让学习Redis的朋友轻松不少; 阅读优秀的源码作品能快速的提升编码内功,而像Redis这样代码量不大(2万多行)
很喜欢这本书的创作过程,以开源的方式,托管到Git上进行创作;
作者通读了Redis源码,并分享了详细的带注释的源码,让学习Redis的朋友轻松不少;
阅读优秀的源码作品能快速的提升编码内功,而像Redis这样代码量不大(2万多行)却句句精致的作品,当然不能错过;
有兴趣的朋友当好好享用;
源码:https://github.com/huangz1990/annotated_redis_source
以下是这本书重点环节的读书笔记;
Redis 使用自行实现的sds 类型来表示字符串:
原因:可以高效地实现追加和长度计算,并且它还是二进制安全的。
在 Redis 内部,字符串的追加和长度计算并不少见,而 APPEND 和 STRLEN 更是这两种操作在 Redis 命令中的直接映射,这两个简单的操作不应该成为性能的瓶颈。另外,Redis 除了处理 C 字符串之外,还需要处理单纯的字节数组,以及服务器协议等内容,所以为了方便起见,Redis 的字符串表示还应该是二进制安全的:
程序不应对字符串里面保存的数据做任何假设,数据可以是以 \0 结尾的 C 字符串,也可以是单纯的字节数组,或者其他格式的数据。
关于sds的详情介绍,参见:
http://origin.redisbook.com/en/latest/internal-datastruct/sds.html#sds
内存映射数据结构:整数集合、压缩链表
内部数据结构:简单字符串(sds)、双端链表、字典、跳跃表
内部数据结构非常强大,但是创建一系列完整的数据结构本身也是一件相当耗费内存的工作,当一个对象包含的元素数量并不多,或者元素本身的体积并不大时,使用代价高昂的内部数据结构并不是最好的办法。为了解决这一问题,Redis 在条件允许的情况下,会使用内存映射数据结构来代替内部数据结构。内存映射数据结构是一系列经过特殊编码的字节序列,创建它们所消耗的内存通常比作用类似的内部数据结构要少得多,如果使用得当,内存映射数据结构可以为用户节省大量的内存。不过,因为内存映射数据结构的编码和操作方式要比内部数据结构要复杂得多,所以内存映射数据结构所占用的 CPU 时间会比作用类似的内部数据结构要多。
集合好用,redis集合支持求交集,求并集操作,让集合的应用范围大幅提升;
但是,需要注意到,求并集的算法复杂度是O(N),而求交集的算法复杂度为O(N的平方),在设计集合存储策略的时候还是尽量少用交集运算;
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以Redis 事务的执行并不是原子性的。如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。另一方面,如果 Redis 服务器进程在执行事务的过程中被停止——比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。当事务失败时,Redis 也不会进行任何的重试或者回滚动作。
因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定。
详见:http://origin.redisbook.com/en/latest/feature/transaction.html#acid
Lua 脚本功能是 Reids 2.6 版本的最大亮点,通过内嵌对 Lua 环境的支持,Redis 解决了长久以来不能高效地处理 CAS(check-and-set)命令的缺点,并且可以通过组合使用多个命令,轻松实现以前很难实现或者不能高效实现的模式。
因为 Redis 命令必须通过客户端来执行,所以需要在服务器状态中创建一个无网络连接的伪客户端(fake client),专门用于执行 Lua 脚本中包含的 Redis 命令:当 Lua 脚本需要执行 Redis 命令时,它通过伪客户端来向服务器发送命令请求,服务器在执行完命令之后,将结果返回给伪客户端,而伪客户端又转而将命令结果返回给 Lua 脚本。
注 这个伪客户端是无网络连接的,那是如何和redis通信的么?是在一个进程中?
和随机性质类似,如果一个脚本的执行对任何副作用产生了依赖,那么这个脚本每次执行所产生的结果都可能会不一样。为了解决这个问题,Redis 对 Lua 环境所能执行的脚本做了一个严格的限制——所有脚本都必须是无副作用的纯函数(pure function)。为此,Redis 对 Lua 环境做了一些列相应的措施:
? 不提供访问系统状态状态的库(比如系统时间库)。
? 禁止使用 load?le 函数。
? 如果脚本在执行带有随机性质的命令(比如 RANDOMKEY ),或者带有副作用的命令(比如 TIME )之后,试图执行一个写入命令(比如 SET ),那么 Redis 将阻止这个脚本继续运行,并返回一个错误。
? 如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
? 用 Redis 自己定义的随机生成函数,替换 Lua 环境中 math 表原有的 math.random 函数和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显式地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。
通过 EXPIRE 、PEXPIRE 、EXPIREAT 和 PEXPIREAT 四个命令,客户端可以给某个存在的键设置过期时间,当键的过期时间到达时,键就不再可用;
当存储的键用于缓存时,通常我们需要设置一个过期时间,到期后由redis删除;
一般为两步:
SET key value
EXPIRE key seconds
有了SETEX,只需要一步就可实现设置值和过期时间:
SETEX key seconds value
进一步想,如果所有的往数据库中增加值的命令都有相应的设置过期时间的函数,岂不是很美好?当然,想归想,实际并非如此,除了SET有SETEX,其它的如集合操作SADD,都没有这样的一步操作命令;
如果一个键是过期的,那它什么时候会被删除?
这个问题有三种可能的答案:
定时删除和惰性删除这两种删除方式在单一使用时都有明显的缺陷:定时删除占用太多 CPU 时间,惰性删除浪费太多内存;
Redis 使用的过期键删除策略是惰性删除加上定期删除, 这两个策略相互配合,可以很好地在合理利用 CPU 时间和节约内存空间之间取得平衡。
参考:http://origin.redisbook.com/en/latest/internal/db.html#id20
rdbSave 函数负责将内存中的数据库数据以 RDB 格式保存到磁盘中,如果 RDB 文件已存在,那么新的 RDB 文件将替换已有的 RDB 文件。在保存 RDB 文件期间,主进程会被阻塞,直到保存完成为止。SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:? SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。? BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。
当 SAVE 执行时,Redis 服务器是阻塞的,所以当 SAVE 正在执行时,新的SAVE 、BGSAVE 或 BGREWRITEAOF 调用都不会产生任何作用。只有在上一个 SAVE 执行完毕、Redis 重新开始接受请求之后,新的 SAVE 、BGSAVE 或BGREWRITEAOF 命令才会被处理。另外,因为AOF写入由后台线程完成,而BGREWRITEAOF 则由子进程完成,所以在SAVE执行的过程中,AOF 写入和 BGREWRITEAOF 可以同时进行。
执行 SAVE 命令之前,服务器会检查 BGSAVE 是否正在执行当中,如果是的话,服务器就不调用 rdbSave ,而是向客户端返回一个出错信息,告知在 BGSAVE 执行期间,不能执行SAVE 。这样做可以避免 SAVE 和 BGSAVE 调用的两个 rdbSave 交叉执行,造成竞争条件。另一方面,当 BGSAVE 正在执行时,调用新 BGSAVE 命令的客户端会收到一个出错信息,告知 BGSAVE 已经在执行当中。
BGREWRITEAOF 和 BGSAVE 不能同时执行:
? 如果 BGSAVE 正在执行,那么 BGREWRITEAOF 的重写请求会被延迟到 BGSAVE 执行完毕之后进行,执行 BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
? 如果 BGREWRITEAOF 正在执行,那么调用 BGSAVE 的客户端将收到出错信息,表示这两个命令不能同时执行。BGREWRITEAOF 和 BGSAVE 两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑:并发出两个子进程,并且两个子进程都同时进行大量的磁盘写入操作,这怎么想都不会是一个好主意。
总的来说:
rdbSave 会将数据库数据保存到 RDB 文件,并在保存完成之前阻塞调用者。
? SAVE 命令直接调用 rdbSave ,阻塞 Redis 主进程;BGSAVE 用子进程调用 rdbSave ,主进程仍可继续处理命令请求。
? SAVE 执行期间,AOF 写入可以在后台线程进行,BGREWRITEAOF 可以在子进程进行,所以这三种操作可以同时进行。
? 为了避免产生竞争条件,BGSAVE 执行时,SAVE 命令不能执行。
? 为了避免性能问题,BGSAVE 和 BGREWRITEAOF 不能同时执行
载入期间,服务器每载入 1000 个键就处理一次所有已到达的请求,不过只有 PUBLISH 、SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五个命令的请求会被正确地处理,其他命令一律返回错误。等到载入完成之后,服务器才会开始正常处理所有命令。
因为 AOF 文件的保存频率通常要高于 RDB 文件保存的频率,所以一般来说,AOF 文件中的数据会比 RDB 文件中的数据要新。因此,如果服务器在启动时,打开了 AOF 功能,那么程序优先使用 AOF 文件来还原数据。只有在 AOF 功能未打开的情况下,Redis 才会使用 RDB 文件来还原数据。
命令到 AOF 文件的整个过程可以分为三个阶段:
redis 目前支持三种 AOF 保存模式,它们分别是:
三种 AOF 保存模式,它们对服务器主进程的阻塞情况如下:
模式 1 的保存操作只会在 AOF 关闭或 Redis 关闭时执行,或者由操作系统触发,在一般情况下,这种模式只需要为写入阻塞,因此它的写入性能要比后面两种模式要高,当然,这种性能的提高是以降低安全性为代价的:在这种模式下,如果运行的中途发生停机,那么丢失数据的数量由操作系统的缓存冲洗策略决定。
模式 2 在性能方面要优于模式 3 ,并且在通常情况下,这种模式最多丢失不多于 2 秒的数据,所以它的安全性要高于模式 1 ,这是一种兼顾性能和安全性的保存方案。
模式 3 的安全性是最高的,但性能也是最差的,因为服务器必须阻塞直到命令信息被写入并保存到磁盘之后,才能继续处理请求。
AOF 重写程序可以很好地完成创建一个新 AOF 文件的任务,但是,在执行这个程序的时候,调用者线程会被阻塞。很明显,作为一种辅佐性的维护手段,Redis 不希望 AOF 重写造成服务器无法处理请求,所以Redis 决定将 AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是:
子进程完成 AOF 重写之后,它会向父进程发送一个完成信号,父进程在接到完成信号之后,会调用一个信号处理函数,并完成以下工作:
当 serverCron 函数执行时,它都会检查以下条件是否全部满足,如果是的话,就会触发自动的 AOF 重写:
事件是 Redis 服务器的核心,它处理两项重要的任务:
Redis 服务器通过在多个客户端之间进行多路复用,从而实现高效的命令请求处理:多个客户端通过套接字连接到 Redis 服务器中,但只有在套接字可以无阻塞地进行读或者写时,服务器才会和这些客户端进行交互。
当服务器有命令结果要返回客户端,而客户端又有新命令请求进入时,服务器先处理新命令请求。
Redis 里面既有文件事件,又有时间事件,那么如何调度这两种事件就成了一个关键问题。简单地说,Redis 里面的两种事件呈合作关系,它们之间包含以下三种属性:
说明:
? 时间事件分为单次执行事件和循环执行事件,服务器常规操作 serverCron 就是循环事件。
? 文件事件和时间事件之间是合作关系:一种事件会等待另一种事件完成之后再执行,不会出现抢占情况。
Redis 以多路复用的方式来处理多个客户端,为了让多个客户端之间独立分开、不互相干扰,服务器为每个已连接客户端维持一个 redisClient 结构,从而单独保存该客户端的状态信息。
当客户端连上服务器之后,客户端就可以向服务器发送命令请求了。从客户端发送命令请求,到命令被服务器处理、并将结果返回客户端,整个过程有以下步骤:
Posted by: 大CC | 11JUL,2014
博客:blog.me115.com [订阅]
微博:新浪微博
原文地址:《Redis设计与实现》读书笔记, 感谢原作者分享。