Redis设计与实现-读书笔记

一:数据结构和对象

SDS:动态字符串 包含len free buf

SDS可以用作AOF缓冲区、客户端状态的输入缓冲区

len:记录buf数组中已使用字节的数量,等于SDS字符串长度

free:记录buf数组中未使用字节的数量

buf:保存字符串

结尾\0,遵循了C字符串的惯例,目的是为了重用部分C字符串的函数

对于C字符串,本身不存储字符串长度,m获取长度需要遍历字符串,复杂度O(n),而访问len属性,复杂度O(1)

杜绝缓冲区溢出:SDS需要修改时,会先检查长度,如果不满足则会扩展,然后再执行

减少内存重分配:C字符中,拼接或缩短字符需要重分配,涉及到系统调用,是比较耗时的操作,SDS通过在buf数组中分配额外空间,避免了这种缺陷

空间预分配:当SDS在修改时,不仅会分配所需要的空间,还会分配额外的未使用空间,分配公式:1.修改后SDS的长度小于1MB,则会分配同样大小的free空间,即两倍 2.如修改后的大于30MB,则会分配1MB free空间,通过预分配减少了内存重分配次数

惰性空间释放:当SDS缩短时,程序不会立即重分配内存,而是使用free属性记录下来,等待将来使用

二进制安全:C字符串是通过空字符串判断是否结束,如果是其他形式的数据则会误认为数据提前结束,SDS都是以二进制的方式处理buf数组里面的数据,并且他使用len属性而不是空字符串来判断字符串是否结束,所以SDS可以存储任意格式的二进制数据。

链表:

每个链表节点为listNode:包含prev next value

list的前置节点和尾节点指向null,所以是无环链表

list可以用于存储不同类型的值

字典:

基本单位dictEntry,如果key冲突,会形成链表,自带hash表 ht0 ht1

数据添加:先根据key计算哈希值,然后计算出key的索引值,放到对应的数组位置上

key冲突:redis采用链地址法,冲突时形成链表,因为没有指向尾节点的指针,所以会放在链表头结点,排在其他已有节点前

rehash:当entry太多或太少时,就需要进行扩容或缩容,步骤:1.为h1分配空间 2.将ht0所有entry rehash到ht1上 3.释放ht0,吧ht1设置为ht0,并为ht1设置一张空白哈希表,为下次rehash做准备

渐进式rehash:rehash是通过多次进行的,为了避免key过多时rehash导致一段时间内停止服务,对性能造成影响

跳跃表:

skiplist是一种有序结构,他通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点的目的

按照成员大小进行排序,如果分值相同按照对象大小进行排序

压缩列表:

ziplist是列表键和哈希键的底层实现之一,当一个列表的元素较少或者字符串长度较短,redis就会用ziplist存储

ziplist是为了节约内存开发的,是由连续内存块组成

对象:

字符串对象:int row embstr , 当保存的是整数值,字符串编码为int,当字符串长度大于32,则用SDS保存,编码为raw,当字符串小于32,用embstr保存,优点是1.内存分配次数由raw的两次降低为一次,2.内存释放函数由raw的两次降低为一次 3.内存连续

long float等浮点类型会作为字符串存储,如果需要计算,则转回浮点类型计算,再讲结果作为字符串保存

列表对象:列表对象的编码可以是ziplist或者linkedlist

当列表对象的字符串长度都小于64字节,或者元素数量少于512个,就会使用ziplist编码,否则使用linkedlist编码,以上两个参数是可以修改的

哈希对象:哈希对象的编码可以是ziplist或者hashtable

ziplist添加过程:先放入key,再放入value,如果再来一个entry,则放在上一个entry后面,如图

如果是hashtable编码,key是字符串,value也是字符串,结构如图

使用ziplist条件:每个字符串小于64字节,键值对数量小于512

集合对象:编码可以是intset和hashtable,当集合对象的所有元素都是整数,或者元素数量少于512个,用intset,否则用hashtable,区别如图

有序集合对象:有序集合的编码可以是ziplist和skiplist

对于zset,是由字典+skiplist/ziplist组成,字典可以再o(1)下找到score值,ziplist结构可以加快范围操作的速度,他们共享成员和分值,用指针指向,因此不会造成内存浪费

内存回收:redis使用引用计数进行内存回收,当new 对象或者被对象被使用时,计数+1,当不被使用时,计数-1,当计数为0,释放对象内存。

对象共享:假如keyA创建了整数value值,如果创建了keyB也是这个值,那么keyB的值指针也会指向那个value,同时值对象计数+1,通过对象共享机制,可以节约内存,注意只有整数值对象会共享

为什么不对字符串对象共享?

因为对比int对象只要o1复杂度,而对比字符串对象的复杂度o(n),当字符串越复杂复杂度越高,越消耗cpu

对象会记录最后一次被访问的时间,这个时间可以用于计算对象的空转时间

redis初始化时会创建0-9999的共享字符串对象

数据库

redis默认会创建16个数据库,默认选择0号,通过select命令切换

在读写命令时,redis还会进行一些维护操作 1.更新命中数,未命中数 2.更新lru时间 3.如果key过期,删除key 4.dirty键计数器+1 触发持久化、复制

过期键的判定:redis维护一张过期字典,key是指向键空间字典的key的指针,value是过期时间

过期键删除策略:

定时删除:创建key时创建一个定时器 过期立即删除 缺点消耗cpu

惰性删除:每次获取key时才判断,过期则删除 缺点消耗内存

定期删除:每段时间进行一次检查,删除里面的过期key

redis使用惰性删除和定期删除

惰性删除实现:每次读写命令都会检查,如果key已过期,则删除

定期删除实现:周期性执行,每次从库中随机取出key,删掉其中过期的Key,然后记录进度,下次接着上次的记录进行处理

RDB和AOF对过期键的处理:

生成RDB:生成rdb文件时会检查数据库中的键,过期键不会被保存到rdb文件中

载入RDB文件:1.主服务器载入:会检查文件中的key,键过期则不会被载入 2.从服务器载入 则会载入所有键,但是在数据同步的时候,从服务器的数据库会被清空,所以一般不会造成影响。

AOF写入:如果key已过期,追加一条DEL命令

AOF重写:重写过程中会对键进行检查,过期键不会被重写

复制:当主机发现key过期,会发送del命令到从机,在未发送del前,即使从机发现key过期,也会像没过期那样直接返回

RDB

有两个命令可以用于生成rdb文件 save和bgsave

save会阻塞线程,直到rdb文件创建完毕,期间redis不能响应任何请求

bgsave会派生出一个子进程,由子进程生成rdb文件

因为AOF的更新频率高,所以如果开启了AOF,优先用AOF还原

bgrewriteaof和bgsave不能同时执行,因为都是子进程执行,如果两个子进程同时对磁盘进行大量写入操作显然会很消耗资源

自动间歇性保存:用户可以自定义多个条件,当满足某个条件则会触发bgsave

dirty计数器和lastsave属性:服务器维护一个dirty计数器和lastsave属性,dirty计数器记录数据库的修改次数,lastsave记录上次持久化成功的时间,每次修改指令执行后会更新dirty计数器的值

检查保存条件是否满足:redis有一个serverCron函数,每100毫秒执行一次,其中一个工作项是遍历所有保存条件,满足一个执行bgsave

AOF

aof通过保存写命令记录数据库状态

AOF持久化的实现:分为三个部分 命令追加、文件写入、文件同步

命令追加:执行完写命令后,写命令将会被追加到aof_buf缓冲区末尾

写入与同步:事件循环中有个一函数,根据appendfsync配置值决定是否将aof_buf写入到aof文件中,默认everysec,则服务器先将aof_buf写入到AOF文件,再进行同步

aways:每次事件循环都会将aof_buf写入aof文件,并且同步aof文件,效率最低安全性最高,服务器宕机时只会丢失一个事件循环的命令数据

everysec:每个事件循环都将aof_buf写入aof文件,并每隔一秒同步aof文件,宕机丢失1秒数据

no:每个事件循环都会将aof_buf写入aof文件,至于何时同步由操作系统决定,写入快但是同步耗时长,平摊下来和everysec效率类似

AOF文件载入与还原:建立伪客户端,在伪客户端不断执行aof文件中的命令

AOF重写:重写的原理是读取文件中key的最新值,用一条批量命令代替原来的多条单个命令,从而减少文件体积

AOF后台重写:会创建子进程进行rewrite,在重写期间,如果某个key被父线程修改了,就会造成数据不一致的问题,为了解决这个问题,redis设置了一个aof重写缓冲区,当redis执行完写命令,这个写命令会同时发送给aof缓冲区和aof重写缓冲区。在重写完成后,子进程会发送信号给父进程,然后父进程将aof重写缓冲区写入到新aof文件,并对新文件进行改名、替换,在整个重写过程中,只有信号处理函数会阻塞父进程

事件

文件事件:redis通过套接字和客户端/其他redis服务器进行连接,服务器与其他客户端/服务器的通信就会产生相应的文件事件,服务器通过监听文件事件来完成一系列网络通信操作

时间事件:redis的一些函数比如serverCron,需要定时执行

文件事件:

redis基于reactor开发了自己的事件处理器,称为文件事件处理器

文件事件处理器使用IO多路复用监听多个套接字,并根据套接字的任务关联不同的事件处理器

当套接字准备执行应答、读、写、关闭等操作,文件事件处理器会调用相关事件处理器来处理

文件事件处理器的组成:套接字、IO多路复用程序、文件分派器、事件处理器

IO多路复用程序监听多个套接字,当产生事件时,将套接字放入队列并传给文件分派处理器,文件分派处理器拿到套接字后分派给相应的事件处理器

redis会在编译时自动选择性能最高的IO多路复用函数来作为IO多路复用程序的底层实现

时间事件:服务器将所有时间事件放在一个无序列表中,遍历执行

运行流程伪代码:

客户端

redis使用一个redisClients结构保存所有客户端信息

客户端属性:

套接字描述符:redisClient里面的fd属性

还有name属性、flag属性

输入缓冲区:querybuffer,用于保存客户端发送的命令请求,可以动态扩缩容,但是大于1GB,则会关闭客户端

输出缓冲区:命令执行结果都会被保存在客户端输出缓冲区,每个客户端有两个输出缓冲区,一个固定,一个可变,固定的保存一些比较小的回复,可变的保存一些比较大的回复

复制

可以通过SLAVEOF命令或者slaveof设置让一个服务器去复制另一台服务器

旧版复制:从服务器发送sync命令给主服务器,主服务器执行bgsave命令生成RDB文件,同时将生成RDB文件期间的写命令写到一个缓冲区,RDB文件生成完后发送给从服务器,从服务器载入文件并进行写数据,执行完后再发送缓冲区的命令,从服务器执行完后达到数据完全同步。

旧版复制的缺陷:当已经同步的主从间网络断开了,过了一会重连成功之后,从服务器重新sync到主,重跑一次同步流程,但是他俩之前已经同步过一次了,真正需要同步的是断连这段时间的写命令,所以sync生成RDB文件是不必要的,而且生成RDB文件会造成大量的内存、CPU、网络消耗。

新版复制:PSYNC,具有完成重同步和部分重同步两种模式,在断网重连的情况下可以只进行断网期间的数据同步,也就是将断开期间主服务器的写命令发送给从服务器

部分重同步功能由三个部分组成:主从的复制偏移量、复制积压缓冲区、服务器运行id

主从都会维护复制偏移量,当主从偏移量不一致时,从将复制偏移量offset发送给主,如果在积压缓冲区中包含offset之后的数据,也就是offset+1开始的数据仍然在复制积压缓冲区,那么进行部分重同步,否则进行完整重同步。

复制积压缓冲区是一个固定大小的FIFO队列,默认1MB,要根据需要调整该缓冲区大小,如果太小,那么PSYNC的重同步模式就不能正常发挥作用(频繁全同步)。

服务器运行id:每个redis服务器都有run id,在启动时自动生成,当从对主进行初次复制时,主会将自己的run id传送给从,从会将这个运行id保存起来,当从服务器断线重连后,发送自己之前保存的主服务器运行id,如果主服务器运行id和从服务器发过来的一致,则进行部分重同步,如果不一样,说明从服务器断线之前连的不是这个主服务器,进行完整重同步操作。

Sentinel

当server1下线,sentinel系统会挑选一个从服务器成为主服务器,并给其他从服务器发送新的指令,将这个从服务器作为新的主服务器,当旧的主服务器重新上线时,会降级为当前主服务器的从服务器。

sentinel会向监控的服务器建立命令连接和订阅连接,sentinel可以通过主或从服务器的订阅频道获取其他sentinel的信息,所以sentinel之间只需要命令连接就够了

主观下线:sentinel会每秒向其他实例(主、从、其他sentinel)发送Ping命令,如果主服务器超过设定时间都返回无效回复时,sentinel会标记主观下线,主观下线不仅针对master,对于slave和其他sentinel同样可以,另外多个sentinel如果设置down_after_millionseconds如果不同,那么他们判定主观下线的时间也会不同

客观下线:当sentinel判定主观下线,他会询问其他sentinel,当收集到足够多的下线,就会开始进行故障转移

领头sentinel选举流程:假如三台sentinel都已经判定客观下线,那么三台sentinel会像其他sentinel发送命令,如果sentinel接收到命令,就会设置领头sentinel,就算其他sentinel再次发来请求成为领头sentinel命令,也会拒绝,然后sentinel可以根据收到命令回复判定有多少sentinel把自己设置为领头sentinel。

故障转移:领头sentinel进行故障转移操作 步骤:1.将从服务器升级为主服务器 2.将原来的主服务器的所有从服务器设置为复制新的主服务器 3.原来的主服务器重新上线后,设置为新的主服务器的从服务器

集群

通过发送cluseter meet命令可以将对应节点添加到集群中

集群的数据结构:clusterNode,保存当前节点的状态 ip port 配置等信息,每个节点都会为自己和其他节点创建clusterNode来保存节点信息。

cluster meet实现:A创建clusterNode结构保存B的节点信息,然后发送MEET消息给B,B收到后也创建clusterNode结构保存A节点信息,然后发送PONG命令返回给A,A再发送PING命令给B,握手完成,之后A会通过Gossip协议发送给其他节点,让其他节点也认识B

槽指派:集群的数据库被分为16384个槽,如果所有槽都有节点处理,则集群处于上线状态(OK),否则处于下线状态(FAIL)通过cluster addslots命令可以给节点指派槽

每个节点都会将自己负责的slots信息通过消息发送给其他节点,然后其他节点会更新自己的slots数组信息,这样每个节点都会知道16384个槽分别分派给了集群中的哪个节点

在集群中执行命令:客户端向集群发送键命令,节点计算该键属于哪个槽,如果是自己处理,则直接执行命令,否则返回MOVED错误,重定向到正确的节点

计算键属于哪个槽:CRC16

节点的数据库实现:集群模式下只能用0号库,并用slots_to_keys跳跃表结构保存槽与键的关系,分值为槽,成员为键

重新分片:可以将某个槽任意指派给其他节点,并且不需要下线,节点也可以继续处理请求

ASK错误:在重新分片期间,可能会有客户端访问正在迁移的键,如果当前键还在原节点,则源节点直接执行,否则返回ASK错误,并重定向到键所在的目标节点执行键命令。

ASK和MOVED的区别:MOVED是指槽i已经转移到另一个节点,客户端接收到MOVED错误后,之后槽i的请求都会路由到正确的节点,ASK是槽迁移过程中的一种临时措施。

复制与故障转移

集群模式分为主节点和从节点,主节点负责处理槽,从节点负责复制主节点,当某个主节点挂掉,其他主节点在挂掉的主节点的从节点中选一个作为新的主节点,新的主节点继续处理槽,旧主节点如果重新上线后成为从节点。

设置从节点:cluster replicate ,当一个节点称为从节点,这一消息会发送给集群的所有节点,最终所有节点都知道这个从节点在复制某个主节点

故障检测:每个节点都会向其他节点定时发送ping消息,如果节点没有回复pong消息,则认为疑似下线,并写错误报告fail_reports,当半数以上主节点认为某个主节点疑似下线,则该节点标记为下线,并向集群广播该节点为下线状态

故障转移:当某个主节点下线时,他的从节点会开始进行故障转移,首先执行slaveof no one命令,称为新的主节点,然后撤销旧主节点的所有槽指派并指向自己,然后向集群广播PONG消息,让所有节点知道自己已经接管了旧主节点的槽指派。

选举:当从节点发现自己复制的主节点下线,他会广播一条消息,让具有投票选择权(处理槽的主节点) 投票给自己,当超过半数的主节点投票给自己,则自己称为新的主节点,如果没有获得足够多的投票,则进入下一个配置纪元重新选举。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值