《Redis 设计与实现》阅读笔记
条评论第 1 版, 此书基于 Redis 2.9.
数据结构与对象
Redis 没有直接使用以下几种数据结构来实现键值对数据库, 而是基于这些数据结构创建了一个对象系统.
Redis 的每个键值对都是由对象组成, 键总是一个字符串对象, 值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象.
sds
Redis 使用 SDS 用作默认字符串表示, C 字符串只会作为字符串字面量用在一些无需对字符串进行修改的地方如打印日志.
linkedlist
链表 (双向无环) 是列表键的底层实现之一.
dict/ht
Redis 的数据库使用字典来作为底层实现, 字典还是哈希键的底层实现之一.
字典 使用哈希表作为底层实现.
哈希表 使用链地址法 (separate chaining) 来解决键冲突.
skiplist
Redis 使用跳跃表作为有序集合键的底层实现之一.
Redis 只在两个地方用到了跳跃表, 一个是实现有序集合键, 一个是在集群节点中用作内部数据结构.
每个跳跃表节点的层高都是 1~32 之间的随机数.
在同一跳跃表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的.
跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象大小进行排序.
intset
整数集合是集合键的底层实现之一.
证书集合的底层实现为数组, 这个数组以有序、无重复的方式保存几何元素, 在有需要的时候, 程序会根据新添加元素的类型,改变这个数组的类型. (类型有INSERT_ENC_INT16/32/64)
整数集合只支持升级操作, 不支持降级操作.
ziplist
压缩列表被用作列表键和哈希键的底层实现之一.
压缩列表是一种为节约内存而开发的顺序性数据结构.
压缩列表可以包含多个节点, 每个节点可以保存一个字节数组或整数值.
在压缩列表中添加新节点或者删除节点, 可能会引发连锁更新操作, 但这种操作出现的几率并不高.
对象
当我们称呼一个键为 “列表键” 时, 我们指的是 “这个数据库键所对应的值为列表对象”.
Redis 的对象系统实现了基于引用计数的内存回收机制, 继而实现了对象共享机制.
Redis 的对象带有访问时间记录信息, 可用于计算数据库键的空转时长, 在服务器启用了 maxmemory 功能的情况下, 空转时长较大的那些键可能会优先被服务器删除.
TYPE 命令返回数据库键对应的值对象的类型, 而不是键对象的类型.
字符串对象是 Redis 五种类型对象中唯一一种会被其他四种对象嵌套的对象.
Redis 只对包含整数值的字符串对象进行共享, 因为尽管共享更复杂对象可以节省更多内存, 但验证操作的时间复杂度也更高.
Redis 在初始化服务器时, 会预先创建一万个字符串对象用于共享, 包含从 0 至 9999 所有整数值.
单机数据库
数据库
过期键删除策略有定时删除、惰性删除、定期删除, Redis 采用的是后两种, 服务器可以很好地在合理使用 CPU 时间和避免浪费内存空间之间取得平衡.
RDB 与 AOF 持久化
RDB 持久化通过保存数据库中的键值对来记录数据库状态, AOF (Append File Only) 持久化通过保存 Redis 服务器所执行的写命令来记录服务器状态.
由于 AOF 文件的更新频率比 RDB 文件更高, 所以如果服务器开启了 AOF 持久化功能, 服务器会优先使用 AOF 文件来还原服务器状态.
SAVE、BGSAVE (background) 命令用于生成 RDB 文件.
当 SAVE 命令执行时, Redis 服务器会被阻塞, 此时客户端所有命令请求都会被阻塞.
而 BGSAVE 命令的保存工作是由子进程执行的, 所以执行时 Redis 服务器仍可以响应客户端请求.
服务器在载入 RDB 文件期间, 会一直处于阻塞状态, 直到载入工作完成.
可通过 save 选项配置 BGSAVE 自动间隔性执行.
Redis 将 AOF 重写程序同样也是放到子程序中执行, 以达到两个目的:
1、子进程进行 AOF 重写期间, 服务器进程 (父进程) 可以继续处理命令请求.
2、子进程带有服务器进程的数据副本, 使用子进程而不是线程, 可以在避免使用锁的情况下, 保证数据的安全性.
但是使用子进程有一个问题, 子进程在进行 AOF 重写期间, 数据库状态可能发生改变, 从而使得重写后的 AOF 所保存的数据库状态已经是过时的了.
为了解决这种数据不一致问题, Redis 服务器设置了一个 AOF 重写缓冲区, 即用来记录重写期间的服务器执行的写命令, 当子进程重写完成后会调用一个信号处理函数通知父进程, 将 AOF 重写缓冲区中的内容写入新 AOF 文件中并原子地覆盖原有 AOF 文件, 使得新 AOF 文件所保存的数据库状态和服务器当前的数据库状态一致.
事件
Redis 服务器是一个事件驱动程序, 其中存在文件事件和时间事件两种事件类型.
文件事件包括可写事件与可读事件.
Redis 基于 Reactor 模式开发了自己的文件事件处理器 (file event handler), 采用单线程 I/O 多路复用.
如果一个套接字可读又可写的话, 服务器会先读再写.
时间事件包括定时事件与周期性事件
目前版本 (2.9) Redis 只使用周期性事件, 没有使用定时事件.
服务器将所有时间事件放入一个无序链表中, 每次需要遍历查找整个链表中已到达的时间事件, 但无序并不影响时间事件处理器的性能.
客户端
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区内, 每个客户端状态都有两个输出缓冲区可用, 一个大小固定, 一个大小可变 (通过使用链表连接多个字符串对象), 前者用于保存那些长度较小的回复 (比如 OK, 简短的字符串值, 整数值, 错误回复等等), 后者用于保存那些长度较大的回复.
服务器
serverCron 函数默认以每 100 毫秒一次的频率更新 unixtime 和 mstime, 所以其精度并不高.
服务器只会在打印日志、更新服务器的 LRU 时间、决定是否执行持久化任务、计算服务器上线时间这类对时间精确度要求不高的功能上使用 unixtime 和 mstime.
对于为键设置过期时间、添加慢查询日志这种需要高精度时间的功能来说, 服务器还是会再次执行系统调用, 从而获得最准确的系统当前时间.
Redis 将服务器进程的 SIGTERM 信号关联到信号关联处理器 sigtermHandler 函数. 服务器在关闭自身之前会进行 RDB 持久化操作, 这也是服务器拦截 SIGTERM 信号的原因, 如果服务器一接到 SIGTERM 信号就立即关闭, 那它就没办法进行持久化操作了.
多机数据库
复制
部分重同步通过复制偏移量、复制积压缓冲区、服务器运行 ID 三个部分实现.
复制积压缓冲区是由主服务器维护的一个固定长度的 FIFO 队列.
Sentinel
Sentinel 本质上是一个运行在特殊模式下的 Redis 服务器.
Sentinel 在连接主服务器或从服务器时, 会同时创建命令连接和订阅连接, 但是在连接其他 Sentinel 时却只会创建命令连, 而不创建订阅连接.
其中命令连接用于向主服务器发送命令请求, 而订阅连接则用于接收指定频道的消息.
用户设置的 down-after-milliseconds 选项的值, 不仅会被 Sentinel 用来判断主服务器的主观下线状态, 还会被用于判断主服务器属下的所有从服务器, 以及所有同样监视这个主服务器的其他 Sentinel 的主观下线状态.
集群
Redis 集群是 Redis 提供的分布式数据库方案, 集群通过分片 (sharding) 来进行数据共享, 并提供复制和故障转移功能.
一个集群通常由多个节点 (node) 组成, 一个节点就是运行在集群模式下的 Redis 服务器.
独立功能
事务
Redis 的事务和传统的关系型数据库事务最大的区别在于, Redis 不支持事务回滚机制, 即使事务队列中的某个命令在执行期间出现了错误, 整个事务也会继续执行下去.
总结
其他参考
- 本文链接:https://antfaiz5z.github.io/2019/05/17/redis/
- 版权声明:The author owns the copyright, please indicate the source reproduced.