redis设计与实现-读书心得
目录
简单动态字符串
链表
字典
跳跃表
整数集合
压缩列表
对象
数据库
RDB持久化
AOF持久化
事件
客户端
服务器
复制
sentinel
集群
发布和订阅
事务
lua脚本
排序
二进制位数组
慢查询日志
监视器
今天完成了第一遍《redis设计与实现》这本书的阅读。一个小小的redis想不到里面有这么多的细节,颇为叹服!现将主要内容作下记录,以备复习使用,也希望能帮到想快速了解redis的朋友。
全书共分四大部分,24个章节。
第一部分:
数据结构与对象
第二部分:
单机数据库的实现
第三部分
多机数据库的实现
第四部分
独立功能的实现
先简单讲了数据结构和对象,然后逐渐深入讲解单机和多机数据库的实现,最后讲了一些独立的功能模块。
这本书之前也曾经尝试拜读过,只不过只看了一两个章节,这次系统学了下,并下载了源码进行了简单的对照学习。
简单动态字符串
Redis只会使用c字符串作为字面量,大多数情况下,用sds作为字符串表示。
比起c字符串,有以下优点:
1、常数复杂度获取字符串长度
2、杜绝缓冲区溢出
3、减少修改字符串时候所需的内存重新分配次数
4、二进制安全
5、兼容部分的c字符串函数
链表
链表被用于redis的各种功能,包括但不限于列表键、发布与订阅、慢查询、监视器等。
每个链表节点由一个listnode组成,每个节点都有一个指向前置节点和后置节点等指针,所以redis等链表是双端链表。每个链表由一个list结构来表示,这个结构包含表头表尾和长度等信息。redis链表是无环链表。
通过为redis 链表设置不同的类型特定函数,可以用于保存各种不同类型的值。
字典
用于实现redis的各种功能,包含数据库和哈希键。
字典使用哈希表作为底层实现,每个字典有两个哈希表,一个平时使用,一个rehash时使用。
当用作数据库或者哈希键的底层实现时,使用murmurhash2算法计算哈希值。
哈希表用链地址法解决冲突,用单向链表。
当哈希表收缩或者扩展时,需要把所有的键值对rehash到新的哈希表里面,这个过程不是一次性完成的,而是渐进式完成的。
跳跃表
是有序集合的底层实现之一,由zskiplist和zskiplistnode两部分组成,前者用于保存表头表尾长度等跳跃表信息,而后者用于保存跳跃表节点。
每个跳跃表节点的层高都是1至32之间的随机数,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
节点按照分值大小排序,分值相同,按成员对象大小排序。
整数集合
整数集合是集合键的底层实现之一。
整数集合的底层实现是数组,这个数组以有序无重复的方式保存数据。有需要时,会根据新添加元素的类型,改变这个数组的类型。
升级操作为整数集合带来了操作上的灵活性,并尽可能的节约了内存,只支持升级操作,不支持降级操作。
压缩列表
压缩列表是一种为节约内存而开发的顺序型数据结构。被用作列表键和哈希键的底层实现之一。
压缩列表可以包含多个节点,每个节点可以包含一个字节数组或者整数值。
添加新节点到压缩列表或删除节点,可能会引发连锁跟新操作,但几率不高。
对象
redis服务器的每个键值对的 键值都是一个对象。
服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,
redis的对象系统是带有引用计数的内存回收机制,当一个对象不再使用的时候,该对象所占的内存就会被释放。
redis会共享0到9999的字符串对象。
对象会记录自己最后一次被访问的时间,这个时间可以用来计算对象的空转时间。
数据库
redis服务器的所有数据库都保存在redisserver.db数组中,数据库的数量保存在redisserver.dbnum属性中。
客户端通过修改目标数据库指针,来指向edisserver.db数组中不同的数据库,达到切换库的目的。
数据库主要由dict和expires两个字典组成,前者保存键值对,后者保存键的过期时间。
数据库由字典组成,数据库操作建立在字典之上。
expires字典的键指向数据库中的某个键,而值记录了过期时间,过期时间是一个以毫秒为单位的时间戳。
redis使用惰性删除和定期删除来删除过期的键。
执行save和bgsave时候保存的文件不包含已经过期的键。执行bgrewriteaof也不包含过期的键。
当主服务器删除过期键的时候,会向所有的从服务器发送一条del命令,显示的删除过期键。从服务器即使发现过期键也不主动删除,等待主节点发来del命令,这种统一,中心化的策略可以保持主从的一致性。
当redis对数据库进行修改后,服务器会根据配置向客户端发送通知。
RDB**持久化**
Rdb文件用于保存和还原redis服务器所有数据库的所有键值对数据。
Save命令会阻塞服务器,由服务器进程直接执行保存操作。bgsave不会阻塞,由子进程执行保存操作。
服务器状态会保存所有用save选项设置的保存条件,当任意一个条件被满足时,服务器会自动执行bgsave命令。
rdb文件是一个经过压缩的二进制文件,由多个部分组成。对于不同类型的键值对,rdb会用不同的方式保存他们。
AOF**持久化**
Aof通过记录服务器的所有的写命令请求保存数据库状态,保存的所有命令都以redis命令请求协议的格式保存。命令会先保存在aof缓冲区,之后再定期写入并同步到aof文件。
appendfsync选项值的不同对aof持久化的安全性和redis服务器的性能有很大影响。
服务器只要载入并重新执行保存在aof文件中的命令,就可以还原数据库本来的状态。
aof重写是产生新的文件,和原来的状态一样,但体积更小。重写是一个有歧义的名字,通过读取数据库中的键值对来实现,无需对现有的aof文件进行读入、分析或写入操作。
在执行bgrewritefof命令时,redis会维护一个aof重写缓冲区,该缓冲区会在子进程创建新的aof期间,记录服务器执行的所有写命令,当子进程完成创建新的aof工作之后,服务器会将所有重写缓冲区中的内容加到新的aof文件的末尾,使得新旧一致,最后服务器用新的aof文件替换旧的文件,完成aof重写操作。
事件
Redis服务器是一个事件驱动程序,服务器事件分为时间事件和文件事件两大类。
文件事件是基于reactor模式实现的网络通信程序。
文件事件是对套接字操作的抽象:每次套接字变为可写、可读或者可应答时,相应的文件事件产生。
文件事件分为ae_readable(读事件) ae_writable (写事件)两类事件,他们之间是合作关系,服务器会轮流处理两种事件,且不会发生抢占。
时间事件分为定时事件和周期性事件两类
服务器在一般情况下只执行servercron函数一个时间事件,并且这个事件是周期事件。
时间事件的处理实际处理时间通常比设定的到达时间晚一些。
客户端
服务器状态结构记录在clients链表里面,新添加的放到链表尾部。
客户端状态的flags属性记录客户端的角色,以及当前客户端所处的状态。
输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不超过1g。
命令的参数和参数个数会记录到会记录到客户端状态的argv和argc里面,而cmd属性里面记录的要执行命令的实现函数。
客户端有固定大小船冲区和可变大小的缓冲区,前者大小16kb,后者不超过设置的硬性限制。
输出缓冲区限制值有两种:
如果缓冲区大小超过了限制,直接关闭客户端,如果在一定时间内,一直超过软性限制,也会关闭客户端。
客户端关闭:1、网络连接关闭 2、发送了不合协议格式的命令请求 3、成为client kill 命令的目标 4、空转时间超时 5、输出缓冲区的大小超出限制
载入aof文件的伪客户端载入完毕后关闭,lua客户端服务器关闭时候关闭。
服务器
一个命令从发送到完成的过程
1)客户端将命令发给服务器
2)服务器读取命令请求,解析命令参数
3)命令执行器根据参数查找命令的实现函数,然后执行函数并得到回复
4)服务器将回复发给客户端
Servercron函数默认每隔100ms执行一次,更新服务器状态信息,处理服务器接收到的sigterm信号,管理客户端资源和数据库状态,检查并执行持久化操作等。
服务器启动到能接收命令前到工作:
1)初始化服务器状态
2)载入服务器配置
3)初始化服务器数据结构
4)还原数据库状态
5)执行事件循环
复制
redis2.8以前的复制功能无法高效的处理断线后重复制情况,2.8增加了部分重同步解决了这个问题。
部分重同步通过复制偏移量,复制积压缓冲区和服务器运行id三个部分来实现。
复制操作刚开始时,从服务器成为主服务器的客户端,发送命令请求执行复制操作,后期,主从互为客户端。
主向从发送命令来保持一致,从向主发送命令进行心跳检测和命令丢失检测。
sentinel
Sentinel只是运行在特殊状态下的服务器,命令表和普通模式下不同。
Sentine会读取指定的配置文件,为要监视的主服务器建立实例结构,并创建连接向主服务器的命令连接和订阅链接,命令连接是发送命令用的,订阅链接是接收指定渠道的信息。
Sentinel通过向主服务器发送info命令获取主服务器下的所有从服务器的信息,并创建实例结构和命令连接和订阅链接。
一般情况下,sentinel按照10s一次的频率向主和从服务器发送info命令,当主下线时候,或者正在对主进行故障转移时,发送频率改为1s一次。
对于监视同一个主和从的多个sentinel来说,他们会每秒一次的频率向被监视的服务的_sentinel_:hello频道发送消息来宣告自己的存在。
每个sentinel会从hello频道取信息,并创建实例和命令连接,
sentinel与主从创建命令和订阅连接,sentinel之间只创建命令连接。
sentinel以每秒一次的频率向其它的主从和sentinel发送ping命令,并根据回复判断实例是否在线,当一个实例在指定的时长中连续向sentile发送无效回复时,判为主观下线。
当判断主观下线时,会向其它的sentinel询问,看他们是否同意这个主观下线。
当收集到足够多的主观下线时候,会定为客观下线,开始故障转移。
集群
节点通过握手将其它节点添加到自己所在的集群
集群中有16384个槽,可以分别指派给集群的各个节点,每个节点都会记录哪些槽分配给了自己,哪些槽被分配给了其它节点。
集群模式下的节点接收一个命令请求,会先看下这个key是否由自己负责,如果不是,返回一个moved错误,会指引客户端转向正在负责该槽点节点。
重新分片由redis-trib负责,主要是将某个槽的所有键值对从一个节点转移到另一个节点。
如果正在迁移,从源节点中没找到要请求的key,返回ask错误,指引客户端去目标节点去找。
moved错误是迁移完成的状态,ask错误是迁移过程中的临时状态。
集群中的节点通过发送消息进行通信,常见的消息有 meet,ping,pong,publish,fail五种。
发布和订阅
redis的订阅可以按照消息订阅,也可以按照模式订阅(可以理解为正则表达式),分别保存在不同类型的链表里。
服务器状态在pubsub_channels字典保存了所有渠道的订阅关系,
服务器状态在pubsub_patterns字典保存了所有模式的订阅关系,
Publish命令通过访问上述两个字典向订阅者发送消息。
Pubsub的三个子命令都是通过读取这两个字典中的信息实现的。
事务
事务可以让一堆命令打包,然后一次性有序的执行。
多个命令会被入队到事务队列中,按照先进先出的方式顺序执行
事务在执行过程中不会中断,当所有消息执行完毕才结束
带有watch命令的事务会将客户端和被监控的key在数据库的watched_keys中做关联,当key被修改时候,程序会将所有被修改的key的客户端的redis_dirty_cas打开。
只有在客户端的redis_dirty_cas未被打开时,服务器才会执行客户端提交的事务,否则,拒绝执行事务。
redis的事务总是具有ACI性质,当appendfsync选项值为always时,才具有持久性。
lua**脚本**
redis会初始化一个lua环境,为了安全去除一些导入文件类等函数,脚本可以执行,可以复制到其他节点,包含script load、script kill、eval、evalsha等。
使用一个伪客户端来执行lua脚本中的redis命令。
服务器在执行脚本之前,会为lua环境设置一个超时处理的钩子,当脚本出现超时运行情况时,客户端可以发送script kill 命令来让钩子停止正在执行的脚本或者发送shutdown nosave 命令来让钩子关闭服务器。
主服务器复制eval, script flush, script load 三个命令的方法和普通的redis命令一样,直接传输即可。
主服务器在复制evalsha时,必须确保所有的从服务器都载入了evalsha命令指定的sha1校验和和lua脚本,如果不能确定,就发送等价的eval命令。
排序
redis可以对键值对的值进行排序,可以按照字母顺序,数字顺序,并且可以对结果取限制的几条和进行保存等。
二进制位数组
采用倒叙的sds结构的方式存放,采用逆序来保存位数组,简化了setbit命令的实现,首先计算够不够存放,然后设置值。
bitcount的实现可以采用查表法和swar方法去优化执行效率。
bitop命令所有的操作都使用c语言的位操作实现。
慢查询日志
用来记录执行超过指定时长的命令。
都记录在slowlog链表中,每个链表节点包含一个slowlogEntry结构,每个结构代表一条慢查询日志。
打印和删除慢查询日志可以通过遍历链表完成。
链表的长度就是保存慢查询的数量。
新的慢查询添加到表头,数量超了会删除。
通过两个参数来控制,一个是slowlog-log-slower-than 单位是微秒,比如超过多少微秒算慢查询,一个是条数 slowlog-max-len,最多可以存多少条慢查询日志,慢查询用先进先出的链表存放,多于这个条数的信息会把最旧的那条删除来存放。
监视器
用户发送 monitor 命令后,就可以成为监视器,客户端发来的命令都会发送一份给所有的监视器。
当一个客户端变监视器时候,redis_monitor表示会被打开。
服务器将所有的监视器都记录到monitors链表中。
每次处理命令请求时,服务器都遍历monitors链表,将相关信息发送给监视器。
还没有评论,来说两句吧...