Davids阅读笔记:《Redis设计与实现》

丁雅惠
2023-12-01

阅读笔记:《Redis设计与实现》

关注可以查看更多粉丝专享blog~

第一部分:数据结构与对象

第一章:(简单动态字符串)

1、Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS(Simple Dynamic String,简单动态字符串)作为字符串表示。
2、比起C字符串,SDS具有以下优点:
2.1:常数复杂度获取字符串长度
2.2:杜绝缓冲区溢出
2.3:减少修改字符串长度时所需的内存重分配次数
2.4:二进制安全
2.5:兼容部分C字符串函数

第二章:(链表)

1、链表被广泛用于实现Redis的各种功能,如列表键、发布订阅、慢查询、监视器等。
2、每个链表节点由listNode表示,每个节点都有指向前置节点和后置节点的指针,所以Redis的链表是双端链表。
3、每个链表都使用一个list来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度信息等。
4、因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表是无环链表。
5、通过为链表设置不同类型的特定函数,Redis链表可以储存不同类型的值。

第三章:(字典)

1、字典被广泛用于实现Redis的各种功能,如数据库和哈希键。
2、Redis中的字典使用哈希表作为底层实现,每个字典都带有两个哈希表,一个平时使用,一个rehash时使用。
3、当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
4、哈希表使用链地址法来解决键冲突,被分配到同一索引上的多个键值对会连成一个单向链表。
5、在对哈希表进行扩展或者收缩时程序需要将现有的哈希表的所有键值对rehash到新哈希表中,并且这个rehash过程并不是一次性完成的,是渐进式的。

第四章:(跳跃表)

1、跳跃表是有序集合的底层实现之一。
2、Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于储存跳跃表信息(如:表头节点、表尾节点、长度),而zskiplistNode用于储存节点信息。
3、每个跳跃表节点的层高都是1-32之间的随机数。
4、在同一跳跃表中,多个节点的分值可以包含相同的分值,但是每个节点的成员对象必须是唯一的。
5、跳跃表中的节点按照分值大小排序,相同分值的按照成员对象大小排序。

第五章:(整数集合)

1、整数集合是集合键的底层实现之一。
2、整数集合的底层实现是数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
3、升级操作为整个集合带来了操作上的灵活性,并且尽可能的节约了内存。(提升编码方式)
4、整数集合只支持升级操作,不支持降级操作。

第六章:(压缩列表)

1、压缩列表是为了节约内存开发的一种顺序型数据结构。
2、压缩列表被用作列表键和哈希键的底层实现之一。
3、压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
4、添加节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。(因为添加了一个表头节点大于254,则第二个节点的previous_entry_length需要由1字节扩展至5字节,若此时超过254则会触发下一个节点因此产生连锁更新,删除节点也是一样头结点大于254,第二个节点小于254,若删除第二个节点,后面的节点刚好超过254,也会引发连锁更新)

第七章:(对象)

1、Redis数据库中每个键值对的键和值都是一个对象。
2、Redis中有字符串、列表、哈希、集合和有序集合五中类型的对象,每种类型的对象至少有两种及以上的编码方式,不同编码方式可以在不同场景上优化对象的使用效率。
3、服务器在执行某些命令前会先检查给定键的类型能否执行指定的命令,而检查键的类型就是检查键的值对象的类型。
4、Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占有的内存就会被自动释放。
5、Redis会共享值为0-9999的字符串对象。
6、对象会记录最后一次被访问的时间,这个时间用于计算对象的空转时间。

第二部分:单机数据库的实现

第一章:(数据库)

1、Redis服务器所有数据库都保存在redisServer.db,而数据库数量则由redisServer.dbnum属性保存。
2、客户端通过修改目标数据库指针,让它指向redisServer.db中的不同元素来切换数据库。
3、数据库主要由dict和expires两个字典构成,其中dict负责保存键值对,expires负责保存键的过期时间。(两个字典的key指向的是同一份数据,节约内存!)
4、因为数据库是由字典构成,所以对数据库的操作都是建立在字典操作之上的。
5、数据库键总是一个字符串对象,而值则可以是任意一种Redis对象,字符串对象(字符串键),列表对象(键),哈希对象(哈希键)、集合对象(集合键)、有序集合对象(有序集合键)。
6、expires字典的键指向了数据库中的某个键,值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。
7、Redis使用惰性删除和定期删除两种策略来处理过期键:惰性删除是只在碰到过期键时才进行删除操作;定期删除则每隔一段时间主动查找并删除。
8、执行SAVE和BGSAVE命令所产生的的RDB文件不会包含过期的键。
9、执行BGREWRITEAOF命令时所产生的AOF文件不会包含过期的键。
10、当一个过期键被删除之后,服务器会追加一条DEL命令到现有的AOF文件的末尾,显式的删除过期键。
11、当主服务器删除一个过期键之后,它会向所有的从服务器发送一条DEL命令,显式的删除过期键。
12、从服务器发现过期键也不会自作主张地删除它,而是等主服务器发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
13、当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。(如:订阅数据库键变化的场景)。

第二章:(RDB持久化)

1、RDB文件用于保存和还原Redis服务器中所有数据库中的所有键值对数据。
2、SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
3、BGSAVE命令由子进程执行保存操作,所以该命令不会阻塞服务器。
4、服务器状态中会保存所有用save选项设置的保存条件,当任意一个条件满足时就会执行BGSAVE命令。(如:save 900 1,save 300 10, save 60 10000)
5、RDB文件是一个经过压缩的二进制文件,由多部分组成。(REDIS db_version databases EOF check_num)
6、对于不同类型的键值对,RDB文件会使用不同的方式来保存它们。

第三章:(AOF持久化)

1、AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
2、AOF文件中所有命令都以Redis命令请求协议的格式保存。
3、命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件。
4、appendfsync选项的不同值对AOF持久化功能的安全性以及服务器性能有很大的影响。
5、服务器只要载入并重新执行AOF文件中的命令,就可以还原数据库本来的状态。
6、AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态是一致的,但体积更小。
7、AOF重写是一个有歧义的名称,该功能是通过读取数据库中的键值对来实现的,程序无须对现有的AOF文件进行任何读取、分析或者写入操作。
8、在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新的AOF文件期间,记录服务器执行的所有写命令,当子进程完成创建新AOF文件的工作后,服务器会将重写缓冲区中所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

第四章:(事件)

1、Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类。
2、文件事件处理器是基于Reactor模式实现的网络通信程序。
3、文件事件是对套接字事件的抽象:每次套接字变为可应答(acceptable)、可读(writable)或者可读(readable),相应的文件事件就会产生
4、文件事件分为AE_READABLE事件(读事件)和AE_WRITABLE(写事件),两类。(acceptable为可读事件)
5、时间事件分为定时事件和周期性事件:定时事件是只在指定时间到达一次;周期性事件是每隔一段时间到达一次。
6、服务器在一般情况下只执行serverCron一个时间事件,而且这是一个周期性事件。(默认每100ms运行一次,Redis 2.8版本之后可通过redis.conf关于hz选项来设置)
6.1:更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
6.2:清理数据库过期的键值对。
6.3:关闭和清理连接失败的客户端。
6.4:尝试进行AOF和RDB操作。
6.5:如果服务器是主服务器,那么对从服务器进行定期同步。
6.6:如果处于集群模式,对集群进行定期同步和连接测试。
7、文件事件和时间事件是合作关系,服务器会轮流执行这两种事件,并且处理过程中也不会出现抢占。
8、时间事件实际处理时间通常会比设定的到达时间晚一些。(原因见第二部分第四章第7节)

第五章:(客户端)

1、服务器状态结构用clients链表连接起多个客户端状态,新添加的客户端状态会被放到链表的末尾。
2、客户端状态的flags属性使用不同标志来表示客户端的角色,以及客户端当前所处的状态。
3、输入缓冲区记录了客户端发送的命令请求,这个缓冲区大小不能超过1GB。
4、命令的参数和参数个数会被记录在客户端状态的argv和argc属性里面,而cmd属性则记录了客户端要执行命令的实现函数。
5、客户端有固定大小缓冲区和可变大小缓冲区,固定大小缓冲区最大大小为16KB,而可变大小缓冲区最大大小不能超过服务器设置的硬性限制值。
6、输出缓冲区的限制有两种,如果输出缓冲区的大小超过了服务器的硬性限制,那么客户端会被立即关闭;除此之外,如果客户端在一定时间内,一直超过服务器的软性限制,那么客户端也会被关闭(无限制:client-output-buffer-limit normal 0 0 0 设置从服务器限制:client-output-buffer-limit slave 256m 64mb 60 设置发布订阅客户端限制:client-output-buffer-limit pubsub 32m 8mb 60)
7、当一个客户端通过网络连接上服务器时,服务器会为这个客户端创建相应的客户端状态,网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区大小超出限制,以上原因都会造成客户端被关闭。
8、处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
9、载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

第六章:(服务器)

1、一个命令请求从发送到完成包括以下步骤:
1.1:客户端将命令请求发送给服务器。
1.2:服务器读取命令请求,并分析出命令参数。
1.3:命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复。
1.4:服务器将命令回复返回给客户端。
2、serverCron每隔100ms执行一次,它的工作主要包括更新服务器状态信息,处理服务器接受SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作。
3、服务器从启动到能够执行客户端命令需要执行以下步骤:
3.1:初始化服务器状态。
3.2:载入服务器配置。
3.3:初始化服务器数据结构。
3.4:还原数据库状态。
3.5:执行事件循环。

第三部分:多机数据库的实现

第一章:(复制)

1、Redis 2.8以前的复制功能不能高效的处理断线后的重复制情况,但是Redis 2.8新添加了部分重同步功能可以解决这个问题。
2、部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。
3、在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器发送命令请求来执行复制步骤,而在复制操作后期,主从服务器会互相成为对方的客户端(执行同步时如果PSYNC为部分重同步,那么主服务器需要成为从服务器的客户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令)。
4、主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测(发送当前偏移量)。

第二章:(Sentinel)

1、Sentinel是运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表,所以Sentinel模式能够执行的命令和普通Redis服务器能够使用的命令不同。
2、Sentinel会读入用户指定的配置文件,为每个要被监视的主服务器创建相应的实例结构,并创建连向主服务器的命令连接和订阅连接,其中命令连接用于向主服务器发送命令请求,而订阅连接用于接收指定频道消息。
3、Sentinel通过向主服务器发送INFO命令获取主服务器属下所有的从服务器信息,并为这些从服务器创建相应的实例结构,以及连向这些从服务器的命令连接和订阅连接。
4、在一般情况下,Sentinel以每十秒一次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者Sentinel正在对主服务器进行故障转移的时候,Sentinel向从服务器发送INFO命令的频率将会改成每秒一次。
5、对于监视同一主服务器和从服务器的Sentinel来说,它们会以每两秒一次的频率,通过向被监视服务器的__Sentinel__:hello频道发送消息来向其它Sentinel宣告自己的存在。
6、每个Sentinel也会从__Sentinel__:hello频道中接收其他Sentinel发来的消息,并根据这些信息为其他Sentinel创建相应的数据结构,以及命令连接。
7、Sentinel只会与主服务器和从服务器创建命令连接和订阅连接,Sentinel与Sentinel之间则只会创建命令连接(Sentinel之间不会创建订阅连接,因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新Sentinel,所以才需要建立订阅连接,而互相已知的Sentinel只需要使用命令连接来进行通信就足够了)。
8、Sentinel以每秒一次的频率向实例(包括主服务器、从服务器、其他Sentinel)发送PING命令,并根据实例对PING命令的回复判断是否在线,当一个实例在指定时长内连续向Sentinel发送无效回复时,Sentinel会将这个实例判断为主观下线。
9、当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它们是否同意这个主服务器进入主观下线状态。
10、当Sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线状态,并对该主服务器进行故障转移操作。

第三章:(集群)

1、节点通过握手来将其他节点添加到自己所处的集群之中。
2、集群中有16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。
3、节点接收到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽节点。
4、对Redis集群的重新分片工作由redis-trib负责执行的,重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
5、如果节点A正在迁移槽i至节点B,那么当节点A没能在自己的数据库中找到命令指定的数据库键值对时,节点A会向客户端返回一个ASK错误,指引客户端到节点B继续查找指定的数据库键。
6、MOVED错误标识槽的负责权已经从一个节点转移到另一个节点了,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施。
7、集群中的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求。
8、集群中的节点通过发送和接受消息来进行通信,常见的消息包括MEET、PING、PONG、PUBLISH、FAIL五种。

第四部分:独立功能实现

第一章:(发布与订阅)

1、服务器状态在pubsub_channels字典中保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面,而UNSUBSCRIBE命令则负责解除客户端和被订阅频道之间的关联。(类似于Java HashMap的结构,数组+链表 key是频道 value是客户端以链表结构存储)
2、服务器状态在pubsub_patterns链表中保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到链表中,而PUNSUBSCRIBE命令负责移除客户端和被退订模式在链表中的记录。
3、PUBLISH命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub_patterns链表来向所有匹配频道的模式的订阅者发送消息。
4、PUBSUB命令的三个子命令(PUBSUB CHANNELS [pattern]返回服务器当前被订阅的频道、PUBSUB NUMSUB [channel-1 channel-2…channel-n]接收任意多个频道作为输入参数,并返回这些频道的订阅者数量、PUBSUB NUMPAT返回服务器当前被订阅模式的数量)

第二章:(事务)

1、事务提供了一种将多个命令打包,然后一次性、有序的执行的机制。
2、多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
3、事务在执行过程中不会被中断,当事务队列中所有命令都被执行完毕后,事务才会结束。
4、带有WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关联(数组加链表,key是键,value是客户端信息),当键被修改时,程序会将所有监视被修改的客户端REDIS_DIRTY_CAS标志打开。
5、只有在REDIS_DIRTY_CAS标志未打开时,服务器才会执行客户端提交的事务,否则的话,服务器将拒绝执行客户端提交的事务。
6、Redis的事务总是具有ACID中的原子性、一致性和隔离性的,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。(不支持错误回滚功能,Redis的原则是简单高效!)

第三章:(Lua脚本)

1、Redis服务器在启动时,会对内嵌的Lua环境执行一系列的修改操作,从而确保内嵌的Lua环境满足Redis在功能性、安全性等方面的需求。
2、Redis服务器专门使用一个伪客户端来执行Lua脚本中包含的Redis命令。
3、Redis使用脚本字典保存所有被EVAL执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
4、EVAL命令为客户端输入的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
5、EVALSHA命令通过直接调用Lua环境中已定义的函数来执行脚本。
6、SCRIPT FLUSH命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
7、SCRIPT EXISTS接受一个或多个SHA1校验和参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
8、SCRIPT LOAD命令接收一个Lua脚本,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
9、服务器在执行Lua脚本之前,会为Lua环境设置一个超时处理的钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送SCRIPT KILL命令来让钩子停止正在执行的脚本,或者发送SHUTDOWN、 nosave命令来让钩子关闭整个服务器。
10、主服务器复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制普通的Redis命令一样,只要将相同的命令传播给从服务器就可以了。
11、主服务器在服务之EVALSHA命令时,必须确保所有从服务器都已经载入EVALSHA命令指定的SHA1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果。(通过repl_scriptcache_dict字典来实现,如果存在则可以传播EVALSHA命令,反之亦然,当有新的从节点加入时则会清空该字典。)

第四章:(排序)

1、SORT命令通过将被排序件包含的元素载入到数组里面,然后对数组进行排序来完成对键进行排序工作。
2、在默认情况下,SORT命令假设被排序键包含的都是数字值,并且以数字值的方式进行排序。
3、如果SORT命令使用了ALPHA选项,那么SORT命令假设被排序键包含的都是字符串值,并且以字符串的方式来进行排序。
4、SORT的排序操作由快速排序算法实现。
5、SORT命令会支持DESC,升序从小到大,降序从大到小
6、当SORT命令使用了BY选项时,命令使用其他键的值作为权重来进行排序操作。
7、当SORT命令使用了LIMIT选项时,命令只保留排序结果集中LIMIT选项指定的元素。
8、当SORT命令使用了GET选项时,命令会根据排序结果集中的元素,以及GET选项给定的模式,查找并返回其他键的值,而不是返回被排序的元素。(先取出指定键的值,进行排序,然后根据键的值,匹配上GET选项指定的规则,当做键去获取值,然后返回取到的键的值返回给客户端,如SORT students ALPHA GET -name,则会先取出所有的students的键值对,然后对值进行快排排序,最终用取出的值匹配-name,然后当做键再次去取值,将再次取到的值一一对应的返回给客户端。)
9、当SORT命令使用了STORE选项时,命令会将排序结果集保存在指定的键里面。(用于不常变化的排序场景SORT students ALPHA STORE sorted_students)
10、当SORT命令同时使用多个选项时,命令限制性排序选项(ALPHA、ASC、DESC、BY),然后执行LIMIT选项,之后执行GET选项,在之后执行STORE选项,最后才将排序结果返回给客户端。
11、除了GET选项以外,调整选项的摆放位置不会影响SORT命令的排序结果。

第五章:(二进制位数组)

1、Redis使用SDS来保存数组。
2、SDS使用逆序来保存位数字,这种保存顺序简化了SETBIT命令的实现,是的SETBIT命令可以在不移动现有二进制的情况下,对位数组进行空间扩展。
3、BITCOUNT命令使用了查表算法和variable-precision SWAR算法来优化命令的执行效率。
4、BITOP命令的所有操作都使用C语言内置的位操作来实现。

第六章:(慢查询日志)

1、Redis的慢查询日志功能用于记录执行时间超过指定时长的命令。
2、Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结构都代表一条慢查询日志。
3、打印和删除慢查询日志可以通过遍历slowlog链表来完成。
4、新的慢查询日志会被添加在slowlog链表的表头,如果日志的数量超过slowlog-max-len选项的值,那么多出来的日志会被删除。

第七章:(监视器)

1、客户端可以通过执行MONITOR命令,将客户端转换成监视器,接受并打印服务器处理的每个命令请求的相关信息。
2、当一个客户端从普通客户端变成监视器时,该客户端的REDIS_MONITOR标识会被打开。
3、服务器将所有的监视器都记录在monitors链表中。
4、每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。

 类似资料: