1、数据结构与对象
1、SDS简单动态字符串(Redis的默认字符串表示)
1、redis需要一个可以被修改的字符串值时就会使用SDS表示字符串值。比如在Redis数据库里,包含字符串值的键值对在底层都是由SDS实现的。
2、SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态的输入缓冲区。
3、SDS遵循C字符串以空字符结尾的惯例,但是保存空字符的1字节空间不计算在SDS的len属性中,空字符对使用者来说时完全透明的,遵循这惯例的好处:SDS可以直接重用C字符串函数库里面的函数。
4、SDS和C字符串的区别:
SDS常数时间复杂度获取字符串长度;
C字符串底层实现是N+1长度的字符数组,要遍历字符串才能获得长度,SDS本身就记录了长度信息,确保获取字符串长度不会成为redis的瓶颈。
SDS可以杜绝缓冲区溢出
SDS的API在修改SDS之前会先检查空间是否足够,不够就会扩展。C中修改字符串如果不手动分配足够空间就可能出现把连续空间的其他内容修改或者时出现溢出。
SDS减少字符串修改带来的内存重新分配次数
对C字符串进行修改总要重新分配内存保存新的字符串。SDS中存储字符的buf字节数组如果空间不足也要重新分配,但是每次增加的空间不一定是修改字符所需的,会额外分配一些未使用空间,用free属性记录。
SDS的两种优化策略:
1、空间预分配
字符长度len超过1M每次分配未使用空间也是1M,小于1M则每次分配与len相同的未使用空间。这样可以减少连续执行增加字符串操作所需的内存分配次数。
2、惰性空间的释放
SDS在缩短字符串的时候并不是立即通过重新分配内存来回收缩短的部分内存,而是将那部分内存当作未使用空间,供将来增长字符串使用,有专门的API用来释放这些内存,所以不担心内存浪费。SDS二进制安全,可以保存文本数据或二进制数据
C字符串必须符合某种编码格式(如ASCII),且以空字符为结束标记,所以中间不能有空字符,所以C字符串只能保存文本数据,不能保存图片、视频等二进制文件。SDS的API都是二进制安全的,使用len的属性值判断字符串是否结束而不是空字符。
SDS兼容部分C字符串函数
2、链表
1、特性
- 双向:每个节点都有前后两个指针;
- 无环:头节点的pre和尾节点的next都指向NULL;
- 带表头和表尾指针,获取表头结点和表尾节点时间复杂度是O(1);
- 带链表长度计数器属性len,获取长度也是O(1);
- 多态,链表节点使用void*指针来保存节点值,并且通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
2、列表键的底层实现之一就是链表;发布与订阅、慢查询、监视器等功能也用到链表;redis服务器使用链表保存多个客户端的状态信息;使用链表来构建客户端输出缓冲区。
3、字典
字典的底层使用哈希表实现,一个哈希表有多个哈希表节点,每个哈希表节点保存字典中的一个键值对。
1、哈希表
哈希表数组table
table保存指向dictEntry结构的指针,每个dictEntry结构保存一个键值对。
哈希表大小size
哈希表大小掩码sizemask,用于计算索引值,总等于size-1
哈希表已有的节点数量used
2、哈希表节点dictEntry
- 键值对的值可以是一个指针,或者uint64_t整数,或者int64_t整数;
- next指针指向链表的下一个节点,因为这里用链地址法解决键的哈希冲突。
3、字典
- dictht ht[2]每一项都是一个dictht哈希表,ht[1]哈希表只在对ht[0]哈希表进行rehash时使用;
- rehashidx记录目前rehash的进度,-1代表没有进行rehash。
4、哈希算法
当新添加键值对到字典中,先用字典设置的哈希函数计算键的哈希值hash,然后使用哈希表的sizemask属性和哈希值计算索引 index=hash & sizemask
5、Redis对字典的哈希表执行rehash
- 为ht[1]分配空间,执行扩展时空间大小为第一个大于等于ht[0].used*2的2^n;执行收缩时空间大小为第一个大于等于ht[0].used的2^n;
- 将ht[0]所有键值对rehash重新计算索引分配到ht[1]上;
- 将ht[1]设置为ht[0],并在ht[1]上新建一个空白的哈希表。
6、哈希表的扩展与收缩
满足以下任意条件则自动开始对哈希表进行扩展操作:
- 服务器目前没有执行BGSAVE或者BGREWRITEAOF,并且哈希表负载因子大于等于1;
- 服务器目前正在执行BGSAVE或者BGREWRITEAOF,并且哈希表负载因子大于等于5。
- 负载因子=哈希表已保存节点数 / 哈希表大小
当哈希表负载因子小于0.1时自动执行收缩操作。
7、渐进式的rehash
利用rehashidx记录ht[0]中已经rehash到ht[1]上的键值对索引值,分多次、渐进式的将ht[0]的键值对慢慢的rehash到ht[1]上,避免集中式的rehash带来的庞大计算量,对服务器性能造成影响。
渐进式rehash期间,字典的删除、查找、更新会同时在两个表进行,新添加的只添加到ht[1]。
8、应用在数据库和哈希键。
4、跳跃表(skiplist)
1、跳跃表是一种有序数据结构,在每个节点维持多个指向其他节点的指针来实现快速访问其他节点的目的。支持平均O(logN)、最坏O(N)复杂度的节点查找,支持顺序性操作来批量处理节点。
2、Redis只在两个地方应用跳跃表:实现有序集合键;在集群节点中用作内部数据结构。
3、跳跃表中zskiplistNode用于表示跳跃表节点,zskiplist用于保存跳跃表节点的相关信息。
zskiplist:包括header表头节点、tail表尾节点、level记录目前跳跃表中层数最大的那个节点的层数、length跳跃表长度(不记头节点)
zskiplistNopde: level层、后退指针、分值、成员对象。
level有两个属性:前进指针和跨度。前进指针用于从表头向表尾遍历,跨度是前进指针所指向节点和 当前节点的距离。层数量越多访问其他节点的速度越快。
跳跃表按照节点分值从小到大排列。double类型的浮点数。分值相同的按照对象的字典顺序排列。
成员对象是一个指针,指向一个字符串对象,该对象保存着一个SDS值。成员对象是唯一的。
4、每个跳跃表节点的层高都是1-32之间的随机数。
5、查找数据的时候先在高层的链表中查找,逐层降低,最终降到第1层精确定位数据位置。查找过程中可以跳过一些节点,加快了查找速度。
6、skiplist和平衡树、哈希表的比较:
- skiplist和平衡树是有序的,哈希表是无序的,所以哈希表只能做精确查找某个key,平衡树和skiplist可以做范围查找;
- 范围查找时平衡树比skiplist操作复杂;
- 平衡树的插入删除导致子树的调整逻辑复杂,skiplist的插入删除只需要改变相邻节点的指针,简单快速;
- skiplist算法实现上比平衡树简单。
5、整数集合(intset)
1、底层实现是一个数组,数组各个项从小到大有序排列,数组不包含重复项。
2、添加新元素时,新元素的类型比现有元素数据类型都长的时候要先对整数集合升级(根据新元素类型扩容、现有元素转换类型、现有元素放置到合适的位置同时保证有序性、新元素添加到底层数组)。每次添加新元素是都有可能引发升级,添加新元素时间复杂度O(N)。
- 升级带来操作上灵活性,添加元素不用担心出现类型错误;
- 升级只在有需要的时候进行,可与尽量节约内存。
3、整数集合不支持降级操作。
6、压缩列表(ziplist)
压缩列表时列表键和哈希键的底层实现之一。
1、压缩列表是Redis为节约内存开发的,是一系列特殊编码的连续内存块组成的顺序型数据结构。可包含任意多个节点,每个节点可以保存一个字节数组或一个整数值。
2、压缩列表节点
previous__entry__length
记录i前一个节点的长度,通过这个指针可以实现压缩列表从表尾向表头遍历操作。
encoding:记录节点content的数据类型和长度
content
3、连锁更新
在特殊情况下添加新节点或删除节点产生连续多次空间扩展操作。
最坏情况下要对压缩列表执行N次空间重分配操作,每次空间重分配的最坏复杂度是O(N),所以连锁更新的最坏复杂度是O(N^2)。
出现几率低:
- 压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能发生。
- 即使出现连锁更新只要被更新的节点数量不多,不会对性能造成任何影响。
8、对象
1、Redis使用对象来表示数据库中的键和值,创建一个键值对至少创建两个对象来分别作为键和值。
2、Redis数据库保存的键值对,键总是字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象其中一种。
3、列表对象的编码可以是ziplist或者linkedlist;
4、同时满足以下两个条件列表对象使用ziplist编码:
- 保存的所有字符串元素长度都小于64字节;
- 元素数量小于512个。
以上两个条件的上限值可以修改。不能满足以上两个条件的需要使用linkedlist。
5、哈希对象的编码可以是ziplist或者hashtable。
使用ziplist编码时,先将保存键的压缩列表节点推入表尾,再将保存了值的压缩列表节点推入表尾。键值对总是紧挨着,先添加的键值对在表头方向。
6、集合对象的编码可以是intset或者hashtable。
编码转换:当集合对象可以同时满足以下两个条件,对象使用intset编码
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。
以上条件中上限值可以在配置文件中修改。
7、有序集合对象的编码可以时ziplist或者skiplist。
ziplist底层使用压缩列表实现,每个集合元素由两个紧挨着的压缩列表节点保存,第一个节点保存元素成员,第二个节点保存元素分值。元素按照分值从小到大排序。
skiplist编码的有序集合底层使用zset实现,一个zset结构同时包含一个字典和一个跳跃表,字典和跳跃表都通过指针共享相同元素的成员和分值,所以同时用这两种结构来保存集合元素不会产生重复元素和分值,也不会因此浪费内存。
为了让有序集合的查找和范围型操作尽可能快的执行,所以选择同时使用字典和跳跃表。字典可以在常数时间查找,跳跃表可以快速实现范围查找。
因为有序集合键的值是哈希对象,所以用于有序集合的命令都是针对哈希对象构建的。
8、特定类型命令在执行之前会通过redisObject结构的type属性进行类型检查。
9、redis在对象系统中构建一个引用计数技术实现内存回收机制,程序通过引用计数跟踪对象的引用信息,在适当的时候自动释放对象并进行内存回收。对象的引用计数属性还带有对象共享的作用。
共享对象只针对包含整数值的字符串对象。因为只有在共享对象和目标对象完全相同的情况下才能实现共享,这需要一个验证的过程,共享对象保存的值越复杂验证的复杂度越高,消耗的CPU时间越多。
10、redisObject还有一个属性lru,记录了对象最后一次被命令程序访问的时间。空转时长是当前时间-lru时间。
服务器如果设置了maxmemory并选择volatile-lru算法或者allkeys-lru算法用于内存回收,当到maxmemory上限值空转时长较高的键会优先被释放。
2、单机数据库的实现
1、数据库
1、Redis服务器默认会创建16个数据库,保存在db数组中。
2、客户端选择不同的数据库是使用SELECT命令,原理是通过修改redisClient.db指针,让它指向服务器中不同的数据库。
3、设置过期时间
- EXPIRE <key> <ttl>:将键key的生存时间设置为ttl秒;
- PEXPIRE <key> <ttl>:…设置为ttl毫秒;
- EXPIREAT <key> <timestamp>:将键key的过期时间设置为timestamp所指定的秒数时间戳;
- PEXPIREAT <key> <timestamp> : …设置为timest毫秒数时间戳。
前三个命令最终都是使用PEXPIREAT命令实现的。
4、TTL命令以秒为单位返回键的剩余生存时间,PTTL则是以毫秒为单位。
5、过期键删除策略
定时删除:设置键的过期时间同时创建一个定时器,让定时器在键的过期时间来临时立即执行对键的删除操作;
定时删除对内存最友好,但是对CPU时间是最不友好的,因此要让服务器创建大量定时器现阶段不现实。占用CPU时间多,影响服务器响应时间和吞吐量。
惰性删除:放任键过期不管,每次从键空间获取键时都检查取得的键是否过期,若过期就删除;
对CPU时间最友好,但是对内存最不友好。浪费内存,有内存泄漏的危险。
定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。
是以上两种的整合和折中。
第一种第三种为主动删除,第二种是被动删除。
6、Redis的过期键删除策略:惰性删除和定期删除配合使用。
- 所有读写Redis数据库的命令在执行之前都会调用expireIfNeeded函数对输入键进行检查,如果过期则将输入键从数据库中删除;
- 定期删除通过activeExpireCycle函数进行:每次运行都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键;全局变量current_db记录当前函数的检查进度,以便下一次函数调用时接着上一次进度处理;最后服务器所有数据库都被检查一遍,_current_db重新置0。
7、生成RDB文件时过期的键会被忽略,主服务器载入RDB文件时会对键进行检查只会载入未过期的键,从服务器则会将所有的键都载入数据库中,在主从同步的时候才会被清空。
8、AOF重写中已过期的键会被检查出来,不会保存到重写的AOF文件中。
9、服务器在复制模式下,从服务器的过期键删除由主服务器控制,只有当主服务器删除了过期键,然后给从服务器发送删除命令,从服务器才会删除过期键,这样来保证主从数据一致。
2、RDB持久化
1、RDB持久化生成的是一个经过压缩的二进制文件。两个命令可以生成RDB文件:SAVE和BGSAVE。
- SAVE是由服务器进程执行保存工作,会阻塞服务器,拒绝客户端的所有命令请求
- BGSAVE是派生一个子进程出来负责创建RDB文件,可以继续处理客户端的命令请求
2、RDB文件的载入是在服务器启动时自动执行,载入期间服务器一直阻塞直到完成。
3、服务器状态维持一个dirty计数器以及一个lastsave属性:
- dirty计数器记录距离上一次成功执行SAVE或BGSAVE命令之后服务器对数据库状态进行了多少次修改(写入、删除、更新等);
- lastsave记录服务器上一次成功执行SAVE命令或BGSAVE命令的时间。
4、间隔性数据保存的实现原理:Redis服务器根据save选项配置的条件,服务器的周期性操作函数serverCron默认每隔100毫秒执行一次,检查save选项设置的保存条件是否满足,满足就执行BGSAVE命令。
5、RDB文件中有一个check_sum,程序载入文件后通过对REDIS、dbversion、databases、EOF_四部分计算出的校验与_check__sum比较,来检查RDB文件是否出错或者损坏。
3、AOF持久化
1、AOF通过保存Redis服务器所执行的写命令来记录数据库状态,写命令以Redis的命令请求协议格式保存。
2、AOF每执行完一个写命令就会以协议格式追加到服务器状态的aof_buf缓冲区末尾,这个命令是包含操作、key、value的。
3、Redis服务器进程就是一个事件循环,服务器每结束一个事件循环之前,会调用flushAppendOnlyFile函数考虑是否需要把aof_buf缓冲区中的内容写入和保存到AOF文件中,主要是服务器配置appendfsync选项来决定不同的持久化行为:
- always:aof_buf所有内容写入并同步到AOF文件中,?效率最慢,安全性最高。
- everysec:aof_buf的所有内容写入AOF文件,距离上次同步超过一秒钟就对AOF同步,同步由一个线程专门负责;?效率足够快,若出现故障停机只会丢失一秒钟的命令数据。
- no:aof_buf的所有内容写入AOF中,但同步由操作系统决定。?写入速度最快但是同步时间最长,而且会在系统缓存中积累一段时间的写入数据,若故障停机会丢失上次同步AOF文件之后的所有写命令数据。
4、AOF文件的载入与数据还原
- 创建一个不带网络连接的伪客户端,因为redis命令只能在客户端上下文中执行,载入AOF文件的命令直接来源AOF文件而不是网络连接;
- 从AOF文件分析并读取一条写命令;
- 使用伪客户端执行被读出的命令;
- 一直执行2、3直到AOF文件中所有写命令都被处理完毕。
5、AOF文件重写
Redis服务器创建一个新的AOF文件代替旧的AOF文件,两个文件保存的数据库状态相同,新的AOF文件中包含的命令更少,所以体积会更小。
- 实现原理:首先从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。重写会忽略空数据库和过期键。
- 为了避免在执行命令时造成客户端输入缓冲区的溢出,重写在处理列表、哈希表、集合、有序集合这四种会带有多个元素的键时,会先检查元素数量是否超过redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量值,如果超过会用多条命令记录这个集合。
- AOF后台重写(BGREWRITEAOF命令)实现原理:AOF后台启动子进程来重写AOF,为了解决父子进程数据不一致的问题,Redis服务器设置了一个AOF重写缓冲区,在子进程重写期间,服务器进程每执行一个写命令都要将其追加到AOF重写缓冲区,子进程完成重写之后向父进程发送信号,父进程收到后就调用一个信号处理函数:将AOF重写缓冲区中的所有内容写入新的AOF文件中,对新AOF文件改名并原子地覆盖现有的AOF文件。调用信号处理函数父进程会阻塞。
4、事件
1、文件事件
文件事件处理器是基于Reactor模式开发的网络事件处理器:
- 使用I/O多路复用程序同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器;
- 当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生,文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
文件事件处理器以单线程方式运行,方便与服务器其他模块对接,保持了Redis内部单线程设计的简单性。使用I/O多路复用实现高性能。
- 文件事件可能会并发出现,I/O多路复用程序会将所有产生事件的套接字放到一个队列中,以此保证有序、同步的每次向文件事件分派器发送一个套接字,处理完一个套接字队列才会继续发送下一个。
- 文件事件的处理器:连接应答处理器、命令请求处理器、命令回复处理器、复制处理器(主从复制时用到)
一次客户端与服务器连接事件实例:
- 客户端发送连接请求,服务器执行连接应答处理器;
- 客户端发送命令请求,服务器执行命令请求处理器;
- 服务器向客户端发送命令回复,服务器执行命令回复处理器。
2、时间事件
主要有定时事件和周期事件。服务器会为时间事件创建全局唯一ID,id从小到大递增;服务器将所有时间事件放在一个无序链表中,当时间事件执行器运行时就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
新的事件总是插入到链表头部,所以链表中事件时有按照ID排序的,无序指的是不按照时间排序。目前版本,服务器最多只是用两个时间事件,所以无序链表几乎退化成一个指针使用,所以不会影响事件执行的性能。
3、事件的调度和执行
- 事件的处理都是同步、有序、原子地执行,服务器不会中途中断,也不会对事件进行抢占;
- 因为时间事件在文件事件之后执行,并且事件之间不会抢占,所以时间事件的实际处理时间通常会比时间事件预定的到达时间稍晚一点。
5、客户端
1、Redis服务器是一对多服务器程序,通过I/O多路复用技术实现的文件事件处理器来保证服务器使用单线程单进程处理多个客户端的命令请求。
2、服务器状态结构使用clients链表连接起多个客户端状态,新添加的客户端被放到链表末尾。
6、服务器
1、serverCron函数默认每隔100毫秒执行一次,主要:更新服务器状态信息、处理服务器接收的SIGTERM信号、管理客户端资源和数据库状态、检查并执行持久化操作。
2、服务器启动到能够处理请求需要先执行:初始化服务器状态、载入服务器配置、初始化服务器数据结构、还原数据库状态、执行事件循环。
3、多机数据库的实现
1、复制
1、旧版复制分同步和命令传播两个操作
2、同步是从服务器向主服务器发送SYNC命令完成:
- 从服务器向主服务器发哦是那个SYNC命令;
- 主服务器收到命令后执行BGSAVE命令,后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
- 主服务器的BGSAVE命令执行完毕后,将RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的状态;
- 主服务器将记录在缓冲区的所有写命令发送给从服务器,从服务器执行这些写命令,保证了主从服务器的数据一致。
3、命令传播:主服务器会将自己执行的写命令发送给从服务器执行,保证主从一致。
旧版复制中如果在命令传播过程从服务器断开连接了,自动重连接之后会发送SYNC命令执行主从同步,效率低下。
4、新版复制使用PSYNC命令代替SYNC命令执行同步操作,PSYNC命令具有完整重同步和部分重同步:
- 完整重同步用于处理初次复制,和SYNC命令步骤一样;
- 部分重同步用于断线后重复制情况:主服务器会将连接断开期间执行的写命令发送给从服务器,从服务器接收并执行这些写命令。
5、部分重同步的实现:
- 主从服务器分别维护一个复制偏移量,主服务器每发送N字节偏移量就加N,从服务器收到N字节偏移量就加N;
- 主服务器维护一个固定长度的先进先出队列,也就是复制积压缓冲区,里面保存一部分最近传播的写命令,并且队列中每个字节都记录了相应的复制偏移量,从服务器断线后重连会向主服务器发送PSYNC命令把自己的偏移量通知给主服务器,主服务器判断该偏移量是否还存在缓冲区,如果存在执行部分同步,否则执行完整重同步。缓冲区大小默认1MB,应该根据需要修改。
- 每个Redis服务器都有自己的运行ID,主服务器在第一次复制的时候把自己的ID发送给从服务器,后续的同步中从服务器发送这个ID给主服务器,主服务器判断这个ID是不是自己,如果是可以尝试执行部分重同步,不是则执行完整重同步。
6、PSYNC命令的实现
- 从服务器如果是首次复制,会向主服务器发送PSYNC ? -1命令请求一次完全复制;
- 非首次复制时,从服务器向主服务器发送PSYNC <runid> <offset>命令,其中runid是上一次同步的主服务器ID,offset则是上一次复制偏移量,主服务器根据这两个参数判断应该执行完全复制还是部分复制。
7、从服务器成为主服务器的客户端之后第一件事就是发送PING命令给主服务器,有以下作用:
- 检查主从服务器建立的套接字链接读写状态是否正常;
- 检查主服务器是否能正常处理命令请求。
8、心跳检测:从服务器默认每秒一次的频率向主服务器发送命令 REPLCONF ACK <replication_offset> 其中offset是当前从服务器的复制偏移量。
主要作用:
- 检测主从服务器的网络连接状态
- 辅助实现min-slaves选项
- 检测命令丢失:通过偏移量可以发现
2、哨兵(Sentinel)
1、哨兵是Redis高可用性的解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器以及这些主服务器下属的所有从服务器,并在被监视的主服务器下线后自动将其下属的某个从服务器升级为新的主服务器,原来的主服务器上线后会被设置为新主服务器的从服务器。
2、初始化Sentinel关键步骤:
初始化Sentinel状态中的masters字典,里面记录所有被Sentinel监视的主服务器的信息;
创建连接主服务器的网络连接,因为需要与多实例创建连接所以使用异步连接。
会创建两个异步网络连接:一个命令连接专门用于向主服务器发送命令并接收命令回复;一个是订阅连接专门用于订阅主服务器的snetinel_:hello频道。创建两个连接是因为:Redis目前的发布与订阅功能中,被发送的消息都不会被保存在Redis服务器里,如果消息发送时接收信息的客户端不在线或者断线,那这个客户端就会丢失这条信息,为了不丢失信息,sentinel专门用一个订阅连接来接收信息。发送命令需要创建命令连接。_
3、sentinel默认每10秒通过命令连接向被监视的主服务器发送INFO命令,通过分析命令的回复获取主服务器当前信息。
4、sentinel发现主服务器有新的从服务器出现,会为其创建相应的实例结构,还创建到从服务器的命令连接和订阅连接。
5、监视同一个主服务器的sentinel还会建立起命令连接,相互进行信息交换。
6、在配置的连续时间内主服务器都向监视的sentinel发送无效回复,则sentinel判定该服务器主观下线,然后sentinel向其他也监视了这台服务器的sentinel发送命令,看其他sentinel是否也认为这台服务器主观下线,如果得到一定数量的回复则sentinel会判定该服务器客观下线,监视这个主服务器的sentinel会协商选一个领头的sentinel,由领头sentinel对下线主服务器执行故障转移操作。
3、集群
1、集群式Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
2、两个节点通过CLUSTER MEET命令实现握手,加入同一个集群中,握手过程:
- 客户端发送命令 CLUSTER MEET <B__ip> <B_port> _给节点A,A为节点B创建一个clusterNode结构,然后给B发送MEET消息;
- B收到MEET消息后,为A创建一个clusterNode结构,并向A返回一个PONG消息,表示收到A的MEET消息了;
- A收到B的PONG消息后将向B发送一条PING消息,表示收到B的PONG消息,两边都确认收到对方消息,这样握手完成。
A会将B的信息通过Gossip协议传播给集群的其他节点,让其他节点也与节点B进行握手。
3、集群通过分片的方式保存数据库中的键值对:集群整个数据库被分为16384个槽(slot),数据库中每个键都属于这16384个槽的其中一个,集群中每个节点可以处理0个或最多16384个槽。
4、槽指派信息不仅保存在各个节点的clusterNode.slots数组中,还保存在clusterState.slots数组中。
如果仅保存在clusterNode.slots数组中,为了直到槽 i是否已经被指派或被指派给哪个节点,程序需要遍历clusterState.nodes字典中所有的clusterNode结构,检查这些结构的slots数组,复杂度O(N);保存在clusterState.slots中只需要访问slots[i]即可,复杂度O(1)。
保存在clusterNode.slots数组的好处:当程序需要把某个节点的槽指派信息发送个其他节点时直接发送这个数组就可以了,如果单独使用clusterState.slots数组的话,每次都得遍历数组才能知道节点的槽指派信息,比直接发送clusterNode.slots数组麻烦低效。
5、节点和单机服务器在数据库方面的区别时节点只能使用0号数据库,而单机Redis服务器没有限制。
6、集群中节点分为主节点和从节点,主节点用于处理槽,从节点复制某个主节点并在主节点下线时代替主节点继续处理命令请求。
7、集群节点之间通过消息来通信。
4、独立功能实现
1、事务
1、Redis通过MULTI、EXEC、WATCH等命令来实现事务,事务是将多个命令请求打包,然后一次性、顺序地执行多个命令请求的机制,并且事务执行期间服务器不会中断事务而去执行其他客户端的命令请求,会把事务中所有命令执行完毕才去处理其他客户端的请求。
2、事务队列是一个multiCmd类型数组,先入队的命令放在数组前面,保证先进先出。
3、WATCH命令是一个乐观锁,在执行事务的命令EXEC执行之前监视任意数量的数据库键,在EXEC执行时检查被监视的键是否至少有一个已经被修改过了,如果是Redis客户端的REDIS__DIRTY__CAS标识被打开,服务器根据标志位是否被打开决定是否拒绝执行事务。Redis数据库保存了一个字典,键是某个被WATCH命令监视的数据库键,值是一个链表,链表元素是所有监视相应数据库键的客户端。
4、事务的ACID
- 原子性:事务队列的命令要么全部执行要么一个都不执行。但是Redis事务不支持回滚,中间某个命令执行错误后续的命令也会被执行,不支持事务回滚时因为Redis追求简单高效,Redis事务执行错误通常是编程错误产生,出现在开发环境中,很少在实际生产环境中出现。
- 一致性:通过错误检测和持久化保证。
- 隔离性:Redis事务总是以串行方式运行,并且服务器保证在执行事务期间不会对事务进行中断。
- 耐久性:当服务器运行在AOF持久化模式下,并且appendfsync选项值为always时,事务才具有耐久性。
2、慢查询
1、两个参数:slowlog-log-slower-than和slowlog-max-len,第一个参数指定执行时间超过多少微秒的命令记录到日志上;第二个参数执行服务器最多保存多少条慢查询日志。
2、到达最大限制数量时,会把最旧的慢查询删除,再保存新的。