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

SDS

数据结构:len, free, buf数组

len记录已使用字节的长度, free记录未使用字节的长度

末尾的‘\0’不计算在len中

C字符串与SDS之间的区别

1、C字符串获取长度的时间复杂度为O(N),SDS为O(1)

2、API不安全,SDS的API安全,不会造成缓冲区溢出,

当缓冲区不足时,SDS会空间预分配,如果修改后的长度小于1MB,free = len;大于1MB,会分配1MB的free

当对SDS字符串缩短时,不会立刻回收内存,而是用free记录

3、C字符串只能保存文本数据,SDS使用二进制方式处理数据,可以保存二进制数据

4、SDS可以使用部分<string.h>的函数

链表

链表节点:有两个指针,一个prev,一个next

链表结构:头节点、尾节点、节点数量和API函数

因此redis的链表是无环的

字典

字典使用哈希表为底层,使用开链法解决哈希冲突

哈希表:有一个table数组,哈希表大小,哈希表掩码(用于计算索引),哈希表的节点数量

哈希表节点:key, value, next指针

字典结构:两个哈希表(用于rehash),rehashindex,私有数据和特定函数

其中rehashindex用于记录rehash的进度

哈希算法:先hashfunc(key)得到哈希值,然后index = hash & mask

rehash:

1、当没有进行BGSAVE和BGREWRITEAOF时,负载因子大于等于1,哈希表拓展,拓展为used * 2 的 2^n

2、当进行BGSAVE和BGREWRITEAOF时,负载因子大于等于5,哈希表拓展

3、当负载因子小于0.1,收缩哈希表,收缩为used 的 2^n

因为进行BGSAVE和BGREWRITEAOF时,redis需要创建服务器的子进程,操作系统进行写时复制,需要避免进行哈希表拓展操作

过程:rehash时,首先对ht[1]进行拓展/收缩,rehashindex = 0,在工作时对字典地增删查改操作中,顺带地将ht[0]中的键值对rehash到ht[1]中,

每rehash一个键值对,rehashindex + 1,最后rehash完成,rehashindex = -1,ht[0]为空并释放ht[0],让ht[1]成为ht[0],再创建空白哈希表成为ht[1]

注:rehash键值对所需工作分摊到增删查改操作中,并且rehash过程中的增只增到ht[1]中,ht[0]只会减少,查找时先查ht[0],再差ht[1]

跳跃表

跳跃表:一个头节点和尾节点指针,记录最大level值(不包含头节点),记录表中len(不包含头节点)

跳跃表节点:分值、后退指针、成员对象、level数组

level数组中包括前进指针和跨度值,

后退指针只能一个一个后退

通过分值大小排序节点,如果相同按照成员对象(指向一个字符串的指针,存放SDS值)在字典中顺序排序

整数集合

数据结构:编码方式,元素数量,数组

其中元素按值从小到大排序,并且不包含重复项

数组的类型由编码方式决定,当添加的新元素的数据类型大于已有元素时,整数集合进行升级

升级:首先扩展数组空间大小,然后将现有元素转换为新的类型,再添加新元素

优点:1、提高数组的灵活性,可以添加任意类型元素;2、节约内存,只有在有需要时才升级

但是,整数集合没有降级操作

压缩列表

压缩列表:zlbytes, zltail, zllen, entry1, entry...., zlend

zlbytes:记录整个压缩列表字节数

zllen:记录节点数

zltail:记录尾节点距离表头的字节,可以快速找到尾节点

zlend:标记末端

列表节点:pre_entry_len,encoding,content

其中pre_entry_len记录前一个节点大小,当前一个节点大于254字节,则pre_entry_len为5字节,否则为1字节

?encoding记录content中数据的个数,00,01,10为字节数组,11为整形?

由于pre_entry_len的变化会带来连锁更新的操作,比如中间删除小节点,或者添加大节点,导致后面如果有连续253字节节点都要拓展

但是影响不大,因为不会有连续的250-253字节的节点,出现连锁更新,更新节点数量也不会很多

对象

Redis中的键和值都是一个对象

键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象

数据结构:type、encoding、ptr、refcount、lru

type指出值对象的类型,encoding记录对象所使用的编码,ptr指向对象底层的数据结构(由encoding决定),refcount是引用计数,lru为空转时长

字符串对象

编码:int, raw, emstr

底层数据结构:SDS

emstr:

emstr是raw的优化版,专门用于保存短字符串

使用raw会调用两次内存分配:object + SDS,也会调用两次内存释放

使用emstr只会调用一次内存分配:object + SDS会放在一块连续的空间中

但是emstr是只读的,操作emstr时,会将emstr转换为raw

对int对象追加一个字符串值,将int转换为raw

注:浮点数作为字符串值保存

列表对象

编码:ziplist,linkedlist

底层数据结构:压缩列表(ziplist),链表(linkedlist)

链表节点中保存的是字符串对象(SDS)

当所有字符串长度小于64字节且元素数量小于512个用ziplist,否则转换为linkedlist

在3.2版本后,底层数据结构只由quicklist实现

哈希对象

编码:ziplist, hashtable

底层数据结构:压缩列表(ziplist),字典(hashtable)

压缩列表:先将新的键推入列表表尾,再将新的值推入列表表尾,保持键在前值在后

字典:每一个键和值都是字符串对象,保存在哈希表中(ht[0])

当所有键值的字符串长度小于64字节且键值对数量小于512个用ziplist,否则转换为linkedlist

集合对象

编码:intset,hashtable

底层数据结构:整数集合(intset),字典(hashtable)

字典:每一个键都是字符串对象,值为NULL

当元素都是整数值且与元素数量小于512个用intset,否则转化为hashtable

有序集合对象

编码:ziplist,skiplist

底层数据结构:压缩列表(ziplist),zset(skiplist)

压缩列表:先推入元素的成员,再推入元素的分值,列表中的元素按分值从小到大排序

zset:包含一个字典和一个跳跃表

先跳跃表中按分值从小到大保存所有元素,object属性保存元素成员,score属性保存分值

然后字典创建了从成员到分值的映射,键为字符串类分值

两种数据结构通过指针共享相同元素的成员和分值,不会浪费内存(在跳跃表中创建,字典关联元素)

当元素数量小于128个且长度小于64字节使用ziplist,否则使用skiplist

为什么用跳跃表 + 字典:

1、如果ZSCORE,则可以从字典中直接查找到相应分值的成员O(1),而跳跃表是O(logN)

2、如果是ZRANK或ZRANGE,则可以用跳跃表返回相应排名和范围内的元素

检查类型和多态命令

执行一个命令时,会检查key的值对象是否为命令所需对象

执行命令也会根据值对象的编码方式,选择正确命令,多态执行

如LLEN命令对列表执行,根据编码ziplist或linkedlist选择相应命令

内存回收和共享内存

内存回收:对象中有一个属性refcount,创建对象时为1,被引用+1,不被引用-1,为0时释放内存

共享内存:如果键A和键B的值相同,则B和A的值指针指向一个现用值对象,对象的引用计数+1(本来A创建是为1,现在为2)

Redis在初始化服务器时,创建0 - 9999的整数值字符串对象,服务器程序已经指向它,引用计数为1,后续用到时,直接共享这些对象

如:SET A 100,这时引用计数为2,因为A和服务器程序都持有这个对象

注:Redis只对包含整数值的字符串对象共享,因为共享时要先验证值对象是否相同,而字符串值的字符串对象验证操作复杂度为O(N)

而包含多个值的对象复杂度更高

空转时长

lru记录了对象最后一次被程序访问的时间,lru共24位

通过当前时间 - lru得到对象的空转时长,OBJECT IDLETIME命令

但是OBJECT IDETIME命令不会修改lru值

当打开了maxmemory选项时,服务器内存超过了maxmemory选项设置的上限,并且回收内存算法为volatile-lru或allkeys-lru时,空转时长高的键会优先被服务器释放

数据库

redis_server里面有一个db数组指针,指向redis的所有数据库,redis服务器默认会创建16个数据库

通过SELECT,底层为调整db指针的位置,可以选择使用的数据库

注:redis没有返回正在使用的数据库名称的命令,在执行重要命令前(FLUSHDB),先SELECT

键空间:服务器中的数据库,里面有一个dict键空间,保存了数据库中的所有键值对;

键空间是一个字典,对数据库的所有操作都是对键空间字典操作

键的过期时间和生存时间

EXPIRE 和 PEXPIRE设置生存时间,一个是秒,一个是毫秒

EXPIREAT 和 PEXPIREAT 设置过期时间,一个是秒,一个是毫秒

上述函数都是使用 PEXPIREAT命令实现的:过期时间 = 当前时间 + 生存时间

TTL 和 PTTL 可以返回键的生存时间和过期时间

PERSIST移除过期时间

删除过期键

数据库中有一个expire过期字典,保存所有键的过期时间,和键空间指向同一个键对象(不浪费空间)

过期键的判定:检查键是否在过期字典中,然后检查UNIX时间戳是否大于键的过期时间

Redis检查是否过期直接检查过期字典,比执行命令(TTL/PTTL)快一点

删除策略:定时器、惰性删除、定期删除

定时器删除使用大量CPU时间,惰性删除对内存不友好

Redis使用惰性删除 + 定期删除

所有Redis命令在执行前都会调用expireIfNeeded函数,判断键是否过期,过期就删除键,因此每个命令的实现都要能处理键不存在的情况

定期删除:规定时间内分多次访问数据库,随机检查一部分键的过期情况

有一个全局变量,记录访问的数据库进度,每一次定期删除都继续上次的数据库进度,直到访问完所有数据库

流程:如果本轮检查的已过期 key 的数量占比随机抽取 key 的数量大于 25%,则继续抽取键 ;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。

为了防止过度循环,定期删除默认不会超过25ms

AOF、RDB、复制 对过期键的处理

已过期的键不会保存到新创建RDB文件中

主服务器载入RDB文件,不保存过期键;从服务器载入保存过期键

写入阶段,先保留过期键,当过期键被删除,程序会向AOF中追加DEL命令

AOF重写阶段,过期键不会保存到重写后的AOF文件中

主从模式:主服务器删除过期键时,会向所有从服务器发送DEL命令,从服务器删除过期键

且从服务器只有收到命令才会删除过期键,保证主从服务器的一致性

数据库通知

通过订阅获知键的变化,分为 键空间通知 和键时间通知

键空间通知:告知键执行了什么命令

键事件通知:告知某个命令被什么键执行了

server.notify.keyspace.event为设置的通知类型

在执行命令时,将它 & 想要发送的通知类型,如果为1则通知,否则不通知

如:执行键空间通知,但没有设置键空间通知,则&的结果为0,不通知

读写键的维护操作

1、更新键命中次数(hit)和不命中次数(miss)

2、更新lru

3、删除过期键

4、维护dirty计数器(通过dirty和lastsvae判断保存条件)

5、数据库通知

RDB持久化

RDB持久化功能将Redis内存中的数据库保存到磁盘中

RDB文件是一个压缩的二进制文件,载入RDB时可以解压缩(检测到RDB文件,自动载入)

SAVE阻塞主线程执行,BGSAVE不阻塞子进程执行

服务器有saveparameters属性,记录了保存条件,如几秒内对数据库进行多少次修改就保存

服务器还维护一个dirty计数器和lastsave属性,dirty记录了上一次SAVE后,服务器对数据库进行了多少次修改,lastsave记录了上一次的保存时间,通过dirty计数器和lastsave判断是否满足保存条件

注:由于AOF文件比RDB文件更新频率高,优先选择AOF文件还原数据

在执行bgsave过程中,Redis可以继续处理命令

因为bgsave由子进程进行操作,和父进程共享同一片内存(相同物理内存,不同的虚拟内存),进行读操作时,不影响;主进程进行写操作时,会进行写时复制,主进程修改的数据会复制一份副本,bgsave子进程会将副本数据写入RDB文件中

RDB文件结构

开头是REDIS,然后是db_version记录了RDB文件的版本号,然后是databases,记录了数据库中的所有键值对,最后是EOF和校验和

databases部分:

包含SELECTDB、db_num和键值对

SELECTDB说明后面是数据库号码,db_num记录了数据库号码,读入了db_num会调用SELECT命令

1、键值对部分:包括TYPE KEY VALUE,如果有过期时间,在TYPE前带上过期时间

2、VALUE部分:每个对象都包括元素个数,每个元素的大小,字符串对象

如:2 5 “HELLO” 1 "A"

1、如果TYPE为字符串对象,超过20字节压缩(压缩算法、原长度、压缩长度、压缩后字符串),小于20字节原样保存

2、如果是哈希对象,则VALUE为 KEY + VALUE

3、如果是有序列表对象,则VALUE为MEMBER + SCORE,SCORE也是字符串对象

4、如果是压缩列表,则载入时先转换为压缩列表,再根据TYPE转换为相应的类型

AOF持久化

AOF是通过服务器所执行命令来记录数据库状态,分为追加、写入和同步三个步骤

追加:服务器执行一个写命令后,写命令会追加到aof_buf中

写入和同步:服务器每次结束一个事件后,都要考虑是否将aof_buf中的内存写入和写回磁盘中(写回磁盘通过系统调用write,由内核完成)

有三种写回磁盘方式:always(效率低), everysec, no(不安全)

AOF重写:

为了解决AOF文件膨胀,提供重写BGREWRITEAOF命令,无需读取AOF文件,直接读取当前服务器状态

并且为了解决数据一致性问题(可能重写时,发生修改数据),Redis设置了AOF重写缓冲区,重写时命令还会追加到重写缓冲区,

重写完成后,将AOF重写缓冲区的内容写入到新的AOF文件中,然后原子性替换现有AOF文件

底层:由后台子进程bgrewriteaof完成,因为多线程需要加锁降低性能;子进程读操作可以共享内存,修改共享内存就会发生写时复制,无需加锁,完成重写操作后,子进程发送信号,主进程收到信号后,将AOF重写缓冲区中数据追加到新的AOF文件中,然后原子性替换现有AOF文件

混合持久化

由于RDB恢复速度快,但快照频率不好把握,容易丢失数据或影响性能

AOF数据恢复慢

所以在AOF重写时,可以开启混合持久化,子进程将与主进程共享的内存数据以RDB形式写入AOF文件,重写完成后,将AOF重写缓冲区中命令以AOF方式写入AOF文件

这样AOF文件前面是RDB,后面是AOF格式

优点:更快速,安全

缺点:不兼容、可读性差

事件

文件事件:通过套接字和客户端通信

时间事件:在给定的时间点执行

文件事件:

AE_READABLE可读事件,AE_WRITEABLE可写事件,两种事件同时到达优先处理可读事件

aeApiPoll(t):阻塞等待文件事件,当至少有一个事件或超时返回

文件事件处理器:

1、连接应答处理器:客户端connect时,产生可读事件,执行相应套接字应答操作,创建通信套接字

2、命令请求处理器:客户端发送请求,产生可读事件,执行套接字读入操作

3、命令回复处理器:当执行完命令产生相应回复时,产生可写事件,执行套接字写入操作

时间事件:

定时事件:指定时间执行一次

周期事件:周期性的执行一次

时间事件属性:时间事件ID,when时间戳,timeProc时间事件处理器

执行时间事件后,如果时间事件处理器返回AE_NOMORE则删除这个时间事件,如果返回其他值,则对when属性进行更新

Redis只使用周期事件,正常只有servercron一个时间事件,处理清除过期键,更新时间和内存等,关闭失效连接,进行AOF/RDB持久化

服务器将所有时间事件放在无序链表中,因为只有一个时间事件,所有无序链表不影响,退化为指针了

事件执行:先获取最接近的时间事件的事件,如果小于0更新为0,然后调用aeApiPoll等待文件事件,如果最近时间事件的时间为0则会立即返回,然后先处理文件事件,再处理时间事件,会造成时间事件的处理比定时时间稍微晚一点

客户端

服务器状态用链表表示多个客户端状态,新的客户端连接放在链表末尾(和全连接队列一样)

客户端状态(不是客户端,在服务端中)的输入缓冲区记录了客户端发送的命令

命令、命令参数记录在客户端状态的argv[]中,argc记录了argv的长度

客户端状态的输出缓冲区记录了命令回复

输出缓冲区分为固定长度缓冲区和可变大小的缓冲区,固定长度数组用完后,开始使用可变大小缓冲区

但是可变大小缓冲区不能超过硬性限制,会关闭客户端;

可以超过软性限制,但是在一定时间内一直超过软性限制时,也会关闭客户端;

伪客户端:1、载入AOF文件时,载入完成关闭伪客户端;2、执行Lua脚本,伪客户端一直存在,直到服务器关闭

服务端

一个命令请求从发送到完成的步骤:

1、客户端发送请求给服务端

2、服务端调用命令请求处理器读取命令请求,并保存到客户端状态的输入缓冲区中,然后对命令请求进行分析,提取出命令,参数等保存到客户端状态的argv[] 和 argc中(argv-[0]为命令)

3、调用命令执行器,通过argv[0]找到命令表(字典),命令表的值为rediscommand结构,里面有一个proc属性为命令的实现函数,然后客户端状态的cmd指针会指向这个rediscommand结构,直接调用client->cmd->proc执行命令

注:命令表的查找算法不区分大小写,所以命令大小写都行

4、调用命令回复器,将命令回复发送给客户端

命令执行器执行前的预备操作:

1、检查cmd指针是否指向空

2、检查命令的参数个数是否正确

3、检查客户端是否通过身份验证

4、如果服务器打开maxmemory,检查服务器内存情况,等等

命令执行器执行的后续操作:

1、添加慢查询日志

2、更新命令中的millisecond 和 calls,记录命令总的执行时长和次数(计算QPS?)

3、如果开启AOF持久化,写入AOF缓冲区中

ServerCorn函数

是一个定期时间函数

1、更新服务器状态中的unixtime属性,减少系统调用次数

2、更新服务器的LRU时钟,因为每个Redis对象都有LRU时钟,服务器 - 对象 得到空转时长

3、更新服务器每秒执行命令次数,通过两次时长内执行命令次数估算出

4、更新服务器内内存峰值记录,超过则更新

5、处理SIGTERM信号,接收到信号,打开标志位,然后关闭服务器,关闭之前先RDB持久化

6、管理客户端资源,清除非活跃连接,如果客户端状态的输入缓冲区超过一定长度,释放并重新创建

7、管理数据库资源,删除过期键,必要时对字典收缩

8、检查持久化操作,先检查是否有AOF重写被延迟,没有的话检查是否满足自动更新,没有的话再判断是否满足AOF重写条件

9、将AOF缓冲区内容写入AOF中

10、增加servercorn的计数器+1

初始化服务器

一个Redis服务器从启动到能接收客户端的命令请求,需要一系列初始化

1、初始化服务器状态,如设置id、端口等

2、载入配置选项,设置默认数据库数量、默认端口等

3、初始化服务器数据结构,如初始化客户端链表,db数组等

4、还原数据库状态,载入RDB/AOF文件

5、执行事件循环

Redis分布式锁

通过SET和lua脚本保证了加锁和解锁

SET lock_key unique_value NX PX 10000 
  • lock_key 就是 key 键;

  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;

  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;

  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

1、加锁:SET + NX(保证原子操作),设置锁(设置过期时间),并且需区分不同客户端的操作

2、解锁:保证操作的客户端是加锁的客户端,先判断unique_value,然后解锁(通过lua脚本)

缺点:不好设置超时时间,主从模式分布式锁不可靠(主节点宕机,新的主节点可以获得锁)

redis通过红锁算法保证集群环境下分布式锁的可靠,即使节点宕机,因为锁的数据在其他节点保存,客户端也可以正常进行锁操作

1、让客户端和多个独立的redis节点一次申请加锁,如果能和半数以上节点加锁,则加锁成功

2、获取锁的总耗时需小于锁的设置时间(设置时间为加锁的设置时间)

如果超时时间来不及完成操作,则释放锁

Lua脚本

Redis通过lua脚本实现原子操作,所以分布式锁解锁需要lua脚本确保解锁的原子性

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

通过redis.call(),在lua脚本中执行redis命令

为什么lua脚本保证原子性:

因为redis在执行脚本时,不会执行其他脚本或redis命令

主从复制

旧版:SYNC,同步 + 命令传播,缺点,当断线重连后,只能完整同步不能部分同步

新版:PSYNC,主从服务器都会为维护一个复制偏移量,且主服务器会维护一个复制积压缓冲区

当从服务器断线后,通过对比偏移量,然后检查从服务器偏移量后的数据是否在积压缓冲区中,如果在则部分同步,不在则完整同步

且部分同步时,还需确认主服务器的运行ID,ID不同则完整同步

PSYNC命令流程:

1、从服务器保存主服务器的IP地址和端口

2、建立套接字连接,从服务器为主服务器的客户端

3、发送PING检查连接状态,回复PONG则继续操作

4、从服务器进行身份验证

5、从服务器向主服务器发送监听端口号(唯一作用打印日志用)

6、同步,同步操作后,主从服务器互为客户端

7、命令传播

心跳检测

在命令传播阶段,从服务器会以每秒一次的频率发送ACK + 偏移量命令

作用:

1、检测网络连接状态

2、检测命令丢失

3、辅助实现mini-slaves(主服务器最小连接数量,延迟的最小时间)

哨兵

哨兵可以监视任意多个主服务器和从服务器,当主服务器下线时,先选出领头哨兵,然后由领头哨兵选出新的主服务器,再让所有从服务器开始复制新的主服务器,最后将旧的主服务器降级为新主服务器的从服务器(故障转移)

  • 哨兵会创建两个连向主服务器的异步网络连接:命令连接 和 订阅连接

  • 哨兵会每隔10秒向监视的主服务器发送INFO命令,更新主服务器的信息

  • 当哨兵接收到其他哨兵的信息时,提取哨兵信息(IP + Port等)然后更新主服务器的sentinels字典

  • 每个与哨兵连接的服务器通过命令连接向频道发送信息,通过订阅连接接收信息,能接收到自己或别的哨兵发送的信息,如果是自己发的则丢弃不处理

  • 当哨兵通过频道发现新的哨兵时,会创建连接新哨兵的命令连接

判断下线

主观下线:哨兵会每秒一次发送PING给所有创建命令连接的实例,如果超过一定时间返回无效回复,则进入主观下线

客观下线:判断为主观下线后,会询问其他监控该服务器的哨兵,如果超过足够数量的主观下线判断,则判定为客观下线

然后进行故障转移

选取领头哨兵

先到先得(发要求成为领头哨兵的命令快),被超过半数的哨兵设置为局部头领则成为领头哨兵

否则再来一轮

选出新的主服务器

正常在线,最近成功与哨兵通信的(INFO),没有过早和旧的主服务器断开的

选出优先级较高的从服务器成为新的主服务器

集群

一个集群由多个节点组成,每个节点都保存一个clusterState

数据结构

clusterNode保存创建节点的时间,标识,IP+端口等

clusterState保存集群节点名单(字典node),集群状态等

槽指派

集群的整个数据库被分为16384个槽,当所有槽都有节点处理时,集群上线

通过CLUSTER ADDSLOTS指派槽给节点负责,实现:先遍历参数中槽看是否都是未指派的,有则返回错误,然后再更新两个slots数组

clusterNode中有slots二进制数组和slots指针数据记录槽的指派信息

节点中slots二进制数组为1则是自己负责的槽,方便发送节点的指派信息

slots指针数组中记录了每个槽指派的节点指针,方便查询槽由哪个节点负责

执行命令

当客户端发送数据库键的命令,节点首先计算出键属于哪个槽,然后检查该槽是否分配给了自己(通过clusterState.myself指针),不是则返回MOVED错误指向正确的节点(自动转向)

键值对除了保存在数据库里之外,还保存在clusterState中的跳跃表中,分值代表槽号,成员为数据库键

重新分片

重新分配槽给其他节点

1、让目标节点准备从源节点导入属于槽的键值对,SETSLOT IMPORTING

2、让源节点准备迁移槽的键值对,SETSLOT MIGRATING

3、从源节点获取最多conut个槽的键值对的键名

4、从3中获取的键迁移至目标节点

然后重复3和4操作

SETSLOT IMPORTING:State中有一个importing[16384]数组记录当前节点正在从其他节点导入的槽,i为槽,importing[i]为节点指针

SETSLOT MIGRATING:State中有一个migrating[16384]数组记录当前节点正在迁移至其他节点的槽,i为槽,migrating[i]为节点指针

ASK错误

如果在重新分片期间,对迁移的槽中的键进行操作,则会检查是否处于源节点的数据库中,如果不是则检查自己的migrating数组看键对应的槽是否正在迁移,如果是则返回ASK错误,引导客户端指向正确的节点(目标节点的IP+端口),然后客户端根据正确的IP+端口找到目标节点,首先发送ASKING命令,然后发送执行命令

ASKING命令就是打开客户端的REDIS_ASKING标志,因为如果节点不负责该槽点就会返回MOVED错误,除非该节点的imoporting数组中正在导入该槽(不为NULL),并且客户端带有ASKING标志,就会破例执行该槽的命令

ASK和MOVED的区别

MOVED标志该槽的负责权已经从一个节点转移到了另一个节点,之后都会指向正确的节点

ASK错误只是临时措施,之后仍指向原来的节点

故障检测

主节点负责处理槽,从节点负责辅助某个主节点

集群中的每个节点都会定期发送PING给其他节点,如果没有收到或者超出规定时间,则将发送节点将接收节点标记为疑似下线,在Node中的flag打开PFAIL标志

各个节点会互相发送消息交换节点的状态信息,Node中记录了一个链表保存其他节点对该节点的下线报告,

当节点知道A被其他节点标记了疑似下线后,就在自己的State.node字典中找到该节点,并在链表中添加其他节点的下线报告,当半数以上节点都标记了疑似下线,则主节点将该节点标记为下线(FAIL),并向集群广播节点的FAIL消息

故障转移

当从节点发现主节点下线时,则进行故障转移,新的主节点会撤销下线主节点的槽指派,并交给自己处理,然后向集群发送PONG告诉其他节点自己成为主节点

选举主节点和哨兵一样(raft算法)

消息

集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEET, PING, PONG, PUBLISH, FAIL

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值