Redis设计与实现读书笔记
2021-05-12 13:49:47 10 举报
AI智能生成
《Redis设计与实现》是一本介绍Redis内部实现原理的书籍。书中详细介绍了Redis的数据结构、持久化机制、网络模型、事务处理等方面的知识,并结合实例讲解了Redis的应用场景和使用方法。通过阅读这本书,读者可以深入了解Redis的内部工作原理,掌握Redis的基本操作技巧,为使用Redis进行实际应用打下坚实的基础。总之,《Redis设计与实现》是一本值得一读的优秀书籍。
作者其他创作
大纲/内容
Redis设计与实现读书笔记
哨兵模式、
Sentinel(哨岗、哨兵)是Redis的高可用性( high availability)解决方案
哨兵模式启动流程
1.初始化服务器
sentinel 本质上还是一个特殊的redis服务器,所以需要先初始化服务器,但与普通的redis服务器有所区别,如sentinel 不需要使用RDB、AOF文件来还原数据库状态
2.使用sentinel 专用代码
用sentinel 专用代码替换普通redis服务器的代码(所以redis的命令对哨兵可能是无效的),如sentinel 默认端口是26379
3.初始化sentinel 状态
在应用了Sentinel的专用代码之后,接下来,服务器会初始化一个sentinel.c/sentinelState结构(后面简称“Sentinel状态”),这个结构保存了服务器中所有和Sentinel功能有关的状态(服务器的一般状态仍然由redis.h/redisServer结构保存)
4.初始化sentinel状态的master属性
Sentinel状态中的masters字典记录了所有被Sentinel监视的主服务器的相关信息,其中字典的键是被监视主服务器的名字。而字典的值则是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构,每个sentinelRedisInstance结构(后面简称“实例结构”)代表一个被Sentinel监视的Redis服务器实例(instance),这个实例可以是主服务器、从服务器,或者另外一个 Sentinel
sentinel状态初始化会引发master属性初始化,而master字典的初始化则是根据被载入的sentinel配置文件来进行的
5.创建连向主服务器的网络连接
初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。
对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:
一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道
为什么需要有两个连接
分支主题
获取主服务器信息
通过这个步骤,哨兵可以得到每个主服务器的从服务器,并将这个信息更新到sentinelState/sentinelReidsInstance/slaves字典。如果出现之前没有记录过的键值对,就知道出现了新的从服务器。
获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。
创建命令连接后,默认每隔10秒一次的频率向从服务器发送INFO命令
通过这些信息,哨兵会更新从服务器的实例字段
向主服务器和从服服务器频道发送信息
每隔两秒向所有被监控的服务器的_sentinel_hello_频道发送如下信息,通过这些信息可以知晓哪个哨兵监控了哪个服务器
接受来自主服务器和从服务器的频道信息
每当哨兵监控一个服务器的时候,就会向这个服务器发送订阅消息,订阅_sentinel_hello_频道。订阅一直持续到连接断开。
通过这些信息,哨兵相互之间就能知道各自的信息。
更新sentinels字典
Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外,所有同样监视这个主服务器的其他Sentinel的资料:
sentinels字典的键是其中一个 Sentinel 的名字,格式为ip:port,比如对于IP地址为127.0.0.1,端口号为26379的Sentinel来说,这个 Sentinel在sentinels字典中的键就是"127.0.0.1 :26379"。
sentinels字典的值则是键所对应Sentinel的实例结构,比如对于键"127.0.0.<br/>1:26379"来说,这个键在sentinels字典中的值就是IP为127.0.0.1,端口号为26379的 Sentinel的实例结构。
当sentinel接受到其他sentinel发送的信息的时候,会根据信息进行如下操作:
如果源Sentinel的实例结构已经存在,那么对源 Sentine1的实例结构进行更新。
如果源Sentinel的实例结构不存在,那么说明源Sentinel是刚刚开始监视主服务器的新Sentinel,目标Sentinel 会为源Sentinel创建一个新的实例结构,并将这个结构添加到sentinels字典里面。
创建连向其他sentinel的命令连接
使用命令连接相连的各个Sentinel可以通过向其他 Sentinel发送命令请求来进行信息交换
检测主观下线状态
在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令判断实例是否在线。
sentinel.config文件下的 down-after-milliseconds 的配置指定了判断一个sentinel实例进入主观下线所需的时间,如果连续这个时间内都返回了无效回复时,就标识这个服务器主观下线,并在master 所对应的实例结构的flags属性中打开SRI_S_DOWN标识
检测客观下线状态
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
发送SENTINEL is-master-down-byaddr
接收SENTINEL is-master-down-byaddr
接收SENTINEL is-master-down-byaddr命令的回复
如果当前哨兵接收到的同意主服务器下线的哨兵的数量大于Sentinel配置文件中quorum参数的值的时候,该哨兵就会认为该主服务器客观下线了。由于不同哨兵的配置文件中quorum参数的值可能有所不同,所以不同哨兵可能对同一主服务器的客观下线有不同认识。
选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel 会进行协商,选举出一个领头 Sentinel,并由领头 Sentinel对下线主服务器执行故障转移操作。
故障转移
在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:
1. 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
2. 让已下线主服务器属下的所有从服务器改为复制新的主服务器。
选出新的主服务器
修改从服务器的复制目标
将旧的主服务器变为从服务器
重点回顾
集群
节点
CLUSTER MEET命令
启动节点
根据cluster-enabled配置选项是否为yes来决定是否开启服务器集群模式
排序
Sort
sort <key>
sort <key> alpha
sort <key> asc/desc
BY
举例
注意,这里并不是用string键值对的值一一对应着set中的值,而是要根据by 中的正则匹配中的*来匹配的,比如如果没有bnana-price,那么就意味着bnana的权重是0,bnana就会排在前面,而如果bnana-price 为10 的话,最后的排序结果就是bnana在最后。
带有alpha选项的BY选项
limit
get
这里的匹配方法也是根据*,类似BY
store
LUA脚本
慢查询日志
slowlog-log-slower-than指定执行时间超过多少微秒的会被记录为慢日志
slowlog-max-than 指定服务器最多保存多少个慢日志记录
监视器
使用MONITER命令将客户端变成一个监视器,会实时的接受和打印服务器当前处理的命令
基础数据结构
简单动态字符串(Simple dynamic string,SDS)
在Redis中的应用
K-V键值对中的键和值都可能是SDS
用作缓冲区:AOF持久化的缓冲区,以及客户端状态中的输入缓冲区。
定义
与C语言中的字符串相比:
获取字符串长度的O(1)时间复杂度
杜绝缓冲区溢出
内存重分配
空间预分配
当SDS的API对一个SDS进行修改并且需要对 SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还为SDS分配额外的未使用空间。
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB。那么程序分配和len属性同样大小的未使用空间,这时 SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)。
如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS 的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30 MB +1MB +1byte。
通过空间预分配,减少了内存重分配的次数,将连续增长N次字符串由原来的必定N次重分配降低为最多N次。
惰性空间释放
当真正需要回收内存时,可以通过API来释放。
二进制安全
SDS不会把字符串中的空字符当作结束。之所以会在最后加上'\\0',是处于想调用C语言的API对字符串进行处理的原因。
链表
List
发布与订阅、慢查询、监视器
字典
Redis数据库,Map
实现
其中dictht是一个哈希表,而dict则是字典,之所以有一个字典有两个哈希表,是为了rehash。而其余属性的含义见下图:
哈希表采用数组+链表的实现,分配节点所在桶时采用位运算,哈希冲突时节点选择头插进链表。(类似JDK1.7的HashMap实现)
rehash
为字典的ht [1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量((也即是ht [0 ].used属性的值)
如果执行的是扩展操作,那么ht [1]的大小为第一个大于等于ht[0].used*2的2的n次方幂
如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0] .used的2的n次方幂
当ht [0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht [0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
渐进式rehash
如果哈希表中的键值对数量太大了,那么久无法短时间把所有的桶都转移到新哈希表上。针对这个问题,服务器并不是一次性的将ht[0]的所有桶都rehash到新表上,而是操作到一个桶就转移一个桶。
在rehash时,查找操作会查找两个哈希表,而添加操作只会添加到ht[1]。
跳跃表
Redis中的应用
zset
属性
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方同),而成员对象较大的节点则会排在后面(靠近表尾的方向)
整数集合
set
encoding:编码方式
length:集合的元素数量
contenes:保存元素的数组,从小到大有序排列
升级
当添加的新元素类型比原有所有元素的类型都长时,就需要进行升级,这样才能将元素添加到集合中。
升级的好处:
提升灵活性
节约内存
压缩列表
List、Map
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点( entry),每个节点可以保存一个字节数组或者一个整数值。
entry
previous_entry_length:记录了压缩列表中前一个节点的长度
encoding:记录了节点的content属性所保存的数据类型和长度
content:保存节点的值
连锁更新
当在数组最前面插入一个值的时候,原本的第一个节点的字节数就会发生改变(previous_entry_length),就有可能出现需要更新previous_entry_length(新节点过大)的情况,而这个更新有可能导致后面所有的节点都更新。
同样,删除节点也有可能导致连锁更新。
对象
前言
基于引用计数的内存回收机制,以及在多数据库时通过引用计数实现共享一个对象。
当在Redis数据库中新建一个键值对时,至少创建了两个对象:一个键对象,一个值对象。
类型
Redis中的每个对象都由一个redisObject结构表示,这个结构的组成:type(类型)、encoding(编码)、ptr(指向底层实现数据结构的指针)
type属性记录了对象的类型
encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是下表列出的常量的其中一个。
每个类型的对象都至少使用了两个不同的编码
所以可以通过encoding属性设定对象使用的编码,而不是固定编码
String
编码方式
如果一个字符串对象保存的是整数值,并且这个值的大小可以用long类型保存,那么字符串对象就会被编码为int
如果字符串对象保存的是一个字符串值,并且这个值大于32字节,那么就使用SDS来保存,并编码为raw
如果字符串对象保存的是一个字符串值,并且这个值小于32字节,那么就使用SDS来保存,并编码为embstr
和raw编码的SDS相比,embstr编码会将reidsObject和sdshdr两个结构一次分配在一个连续的空间
常见命令
SET GET
APPEND
INCRBYFLOAT
INCRBY
DECRBY
STRLEN
SETRANGE
GETRANGE
setnx setex
mset msetnx
mget
getset
LPUSH RPUSH
LPOP RPOP
LINDEX
LLEN
LINSERT
LREM
LTRIM
LSET
LRANGE RRANGE
Map
在使用压缩列表的编码方式时,Map的键-值对在压缩列表中是相邻的,键在前,值在后
HSET HGET
HEIXSTS
HDEL
HLEN
HGETALL
HKEYS
HVALS
HINCRBY
HSETNX
HMSET HMGET
Set
SADD
SCARD
SISMEMBER
SMEMBERS
SPOP
SREM
ZSet
在使用跳表做zset的数据结构的时候,还需要用map,这样可以保证查找一个元素时间复杂度在O(1)且可以进行有序查找。否则只用map就会无序,只用跳表就需要遍历查值。
ZADD
ZCARD
ZCOUNT
ZRANGE
ZREVRANGE
ZRANK
ZREVRANK
ZREM
ZSCORE
Redis会在服务器初始化的时候,创建0~9999的所有整数,并将这些整数共享,而不是每次都创建新的
数据库
数据库数组
初始化为16个数据库,可以通过select命令更改当前数据库
常用命令
FLUSHDB 删除当前数据库所有键值对
RANDOMKEY 返回一个随机的键
DBSIZE 返回键空间包含的键值对的数量
RENAME 重命名
键空间
键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象的任意一种Redis对象
expires字典
过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)。
过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的 UNIX时间戳(expire、pexpire、expireat命令的时间都会转换成pexpireat)
EXPIRE PEXPIRE 设置键的生存时间 单位秒/微秒
EXPIREAT PEXPIREAT 设置键的过期时间 单位秒/微秒
TTL PTTL 接受一个带有生存时间或过期时间的键,返回这个键的剩余生存时间
PERSISIT 移除一个键的过期时间
过期键删除策略
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
Redis过期键删除策略
Redis中配合使用惰性和定期删除策略
惰性删除策略的实现
过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
如果输人键已经过期,那么expireIfNeeded函数将输入键从数据库中删除。
如果输人键未过期,那么expireIfNeeded 函数不做动作。
定期删除策略的实现
RDB、AOF和复制功能对过期键的处理
RDB
写入
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入:
如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影响。
如果服务器以从服务器模式运行,那么在载人RDB文件时,文件中保存的所有键不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
AOF
重写
主从复制
由主服务器来控制从服务器统一删除过期键
RDB持久化
创建与载入
创建
SVAE
会阻塞Redis服务器进程,知道RDB文件创建完毕,在创建期间Reids服务器不能接受任何请求
BGSAVE
BGSAVE会派生出一个子进程然后由子进程负责创建RDB文件
在BGSAVE执行的时候,不允许再执行SAVE、BGSAVE等指令
自动save
save条件
自动save的触发
serverCron函数默认每个100ms执行一次,该函数会对正在运行的服务器进行维护,它的其中一项工作就是坚持server选项所设置的保存条件是否已经满足,满足就会自行BGSAVE
使用dirty属性记录自上次save之后数据库中数据的修改次数,lastsave记录上次成功执行save命令的时间
载入
会在Redis服务器启动时自动执行,如果检测到RDB文件存在,就会自动载入
并且如果开启了AOF持久化,会优先使用AOF来还原数据库
AOF持久化
AOF实现原理
命令追加
aof_buf
文件写入
Redis的服务器进程就是一个事件循环( loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF 文件里面,这个过程可以用以下伪代码表示:
文件同步
AOF文件的载入与数据还原
AOF重写
将多个命令整合进一个命令
BGREWRITEAOF
为了解决由子进程执行重写导致的数据库状态不一致情况,设置了一个AOF重写缓冲区
复制
使用SLAVEOF命令让一个服务器去复制另一个服务器,被复制的是主服务器,进行复制的是从服务器
复制的方法
同步
完整重同步(SYNC)
适用于第一次进行数据库复制或者更改复制的主数据库,将数据库整体复制,较为花时间
部分重同步(PSYNC)
适用于从服务器断线之后重连的情况,只复制断开期间执行的命令
命令传播
适用于主从服务器的数据库状态一致后,对主服务器执行的命令发送到从服务器
部分重同步的实现
复制偏移量
主从服务器都维护一个复制偏移量,记录当前执行命令的字节数,在主服务器执行命令、从服务器复制的时候更改
可以记录从服务器相较于主服务器缺少的命令有多少
复制积压缓冲区
默认为1M的固定长度先进先出队列
在服务传播的时候,会记录传播的命令。这样如果主从服务器复制偏移量之差小于1M的情况下,只需要将缺少的命令从缓冲区复制到从服务器即可,不用完整重同步
服务器运行ID
每个服务器开启时都有一个随机的ID,从服务器会记录主服务器的ID。断线重连的时候会带上之前的主服务器ID,如果主服务器ID与接收到的ID一致,就可以确定发送方是从服务器,然后根据复制偏移量之差决定是否采用部分重同步。
复制的实现
1.设置主服务器的地址和端口
2.建立套接字连接
3.发送PING命令
4.身份验证(masterauth密码验证)
5.发送端口信息
6.同步
7.命令传播
心跳检测
在命令传播阶段,从服务器会以每秒一次的频率向主服务器发送命令
检测主从服务器的网络连接状态
主从服务器可以通过发送和接收REPLCONF ACK命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,那么主服务器就知道主从服务器之间的连接出现问题了。
辅助实现min-slaves配置选项
检测命令丢失
0 条评论
回复 删除
下一页