redis知识点汇总

news2024/11/16 1:37:01

     

一、Redis的数据类型和数据结构

1、Redis五种数据类型

String(字符串)、List(列表)、Hash(哈希)、Set(集合)和Sorted Set(有序集合)。

2、Redis的底层数据结构

简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。

二、redis的key/value结构与实现

 

redis是基于hash即字典实现的简单如下:

RedisDb是Redis的底层数据结构的开始,里面存放着Redis的数据,一般默认有16个,这个是可以配置的,Redis的数据是以字典的形式在底层展示的。

dict定义如下


typedef struct dict {

   dictType *type;  // dictType结构的指针,封装了很多数据操作的函数指针,使得dict能处理任意数据类型,为了支持各种数据类型封装了各种操

   dictht ht[2];  // 两个hashtable,ht[0]为主,ht[1]在渐进式hash的过程中才会用到。 

   long rehashidx; /* 增量hash过程过程中记录rehash执行到第几个bucket了,当rehashidx == -1表示没有在做rehash */

   unsigned long iterators; /* 正在运行的迭代器数量 */

} dict;


dictht定义如下


typedef struct dictht {

   dictEntry **table;  // hashtable中的连续空间

   unsigned long size; // table的大小

   unsigned long sizemask;  // hashcode的掩码 

   unsigned long used; // 已存储的数据个数

} dictht;


dictEntry的定义如下


typedef struct dictEntry {

   void *key;  //具体表示Key-Value形式的数据结构的key,底层就是SDS

   union {   // dictEntry在不同用途时存储不同的数据

       void *val; //具体表示Key-Value形式的数据结构的value数据结构

       uint64_t u64;

       int64_t s64;

       double d;

   } v;

   struct dictEntry *next; // hash冲突时开链,单链表的next指针

} dictEntry;


三、list与Zset实现及结构

1、List结构

        在redis3.0中,List的底层是通过双向链表和压缩列表来实现的,但是由于C 语言本身没有链表这个数据结构的,所以 Redis 自己设计了一个链表数据结构。在3.0之后,List是通过quicklist来实现的

3.0链表

结构优缺点

优点:获取一个节点的前置节点、后置节点,以及获取链表的头节点和尾节点的时间复杂度都是O(1)。获取链表长度的时间复杂度是O(1)。

listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值

 

缺点:链表节点之间的内存是不连续的,所以无法很好利用 CPU 缓存。

当数据比较少时,比如只有一个节点,也需要一个链表节点结构头(list)的分配,内存开销较大。因此,List 对象在数据量比较少的情况下,会采用压缩列表作为底层数据结构的实现。

3.0之后链表实现

quicklist 的结构体跟链表的结构体类似,都包含了表头和表尾,区别在于 quicklist 的节点是 quicklistNode。quicklist与quicklistNode的结构如下


typedef struct quicklist {

    quicklistNode *head;      //quicklist的链表头

    quicklistNode *tail;    //quicklist的链表尾

    unsigned long count; //所有压缩列表中的总元素个数

    unsigned long len;         //quicklistNodes的个数

    ...

} quicklist;


typedef struct quicklistNode {

    struct quicklistNode *prev;     //前一个quicklistNode

    struct quicklistNode *next;     //后一个quicklistNode

    unsigned char *zl;            //quicklistNode指向的压缩列表  

    unsigned int sz;              //压缩列表的的字节大小   

    unsigned int count : 16;          //压缩列表的元素个数

    ....

} quicklistNode;


在quicklistNode中,链表节点的元素不再是单纯保存元素值,而是保存了一个压缩列表,所以 quicklistNode 结构体里有个指向压缩列表的指针 zl。

在向 quicklist 添加一个元素的时候,不会像普通的链表那样直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。

2、Zset结构

Zset是有序集合,它在set的基础上加了一个值score称为权重,可以通过score进行排序。在redis3.0之前,Zset底层使用的是压缩列表和跳表,之后使用的是跳表和listpack

3.0之前实现

压缩列表ziplist:满足以下两个条件[value,score] 

1、键值对数量少于 128 个。

2、每个元素的长度都不大于 64 字节

skiplist:不满足以上两个条件时使用跳表、组合了 hash 和 skiplist hash 用来存储 value 到 score 的映射,这样就可以在 O(1) 时间内找到 value 对应的分数skiplist 按照从小到大的顺序存储分数skiplist 每个元素的值都是 [value,score] 对

压缩列表zipList结构如下:

 

跳表之前已经讲过不在赘述

3.0之后实现

listpack的目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。

listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量。

每个 listpack 节点结构如下:

 

Encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编;data,实际存放的数据;len,encoding+data的总长度;

四、redis事件驱动

          redis 根据不同系统构建了不同的多路复用实现:例如linux的epoll,OS X的kqueue,windows的select。Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport、kqueue这些I/O多路复用函数库来实现的。

1.文件事件处理器

    Redis文件事件处理器,文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行 连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生,此时,文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用I/O多路复用程序来监听多个套接字, 又可以很好地与Redis服务器中其以单线程方式运行的模块进行对接, 这保持了 Redis内部单线程设计的简单性。

文件事件处理器的四个组成部分:

1、套接字  2、I/O多路复用程序  3、文件事件分派器(dispatcher)   4、事件处理器

例如:

 

为了对连接服务器的各个客户端进行应答, 服务器要为监听套接字关联【连接应答处理器】。

为了接收客户端传来的命令请求, 服务器要为客户端套接字关联【命令请求处理器】。

为了向客户端返回命令的执行结果, 服务器要为客户端套接字关联命令【回复处理器】。

当主服务器和从服务器进行复制操作时, 主从服务器都需要关联特别为复制功能编写的【复制处理器】。

2.常见的多路复用库

主要有以下几种常见的多路复用函数库

1、select:服务端一直在轮询、监听如果有客户端链接上来就创建一个连接放到数组A中,继续轮询这个数组,如果在轮询的过程中有客户端发生IO事件就去处理;select只能监视1024个连接(一个进程只能创建1024个文件);而且存在线程安全问题;select主要在windows系统中

poll:在select做了许多修复,比如不限制监测的连接数;但是也有线程安全问题;

epoll:也是监测IO事件,但是如果发生Io事件,它会告诉你是哪个连接发生了事件,就不用再轮询访问。而且它是线程安全的,但是只有linux平台支持;

select直接吧布尔数组拷贝到内核态,由内核来进行while遍历,检测所有的fd是否有数据生成,如果哪个fd有数据生成,就将对应位的布尔数组置1 ,并返回,不再阻塞。如果没有任何的fd有数据,那么内核就一直循环,成阻塞状态。

内核态跳出循环之后,我们便对布尔数组进行遍历,看看它到底是哪一个fd有数据了,之后去除这个fd,读取数据。select输入参数会输入fd数组的长度,用来卡布尔数组的长度,因为bitmap的默认长度是1024,所以给卡一下长度。每次布尔数组用完之后,需要对布尔数组进行信号清零,这会消耗一定计算资源。

 select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024,虽然可以调整大小,但是它依然是有上限的。


 

poll函数的输入参数要比select少一些。

同样,poll函数也是阻塞的,也是内核进行fd遍历,之后进行置位。

但是它置位,置的是一个专门的revents字段,如果fd有数据就将revents置位为POLLIN。(pollfd结构体是第一个输入参数)。

而且,由于它传入的是结构体,结构体之间由链表相连接,所以但是它没有最大连接数的限制。

poll本质上和select没有区别

首先,内核态创建epoll实例,创建红黑树和就绪链表。

对红黑树进行操作,装填所有的socket。

开启一个阻塞线程while

红黑树中哪一个socket就绪了,就把他加载到就绪链表中。

就序列表复制内容到events中,完成内核态到用户态的数据传输。

用户态收到了有数据的套接字fd,进行后续数据操作即可。

假设有100个文件描述符,有10个数据进来了。select和poll要轮询完这100个文件描述,才知道,这些文件描述符,具体是哪10个有数据可以读。

而Epoll的话,epoll_wait会将已经准备好的10个文件描述符放到链表的最前面,告诉用户态直接去读最前面的10个文件描述符就可以,不用遍历所有100文件描述符

而 Redis 针对不同操作系统,会选择不同的 IO 多路复用机制来封装事件驱动框架,具体代码见 ae.c。

// ae.c

#ifdef HAVE_EVPORT

#include "ae_evport.c"  // Solaris

#else

    #ifdef HAVE_EPOLL

    #include "ae_epoll.c"   // Linux

    #else

        #ifdef HAVE_KQUEUE

        #include "ae_kqueue.c"  // MacOS

        #else

        #include "ae_select.c"  // Windows

        #endif

    #endif

#endif

仔细看上面的代码逻辑,先判断了 Solaris/Linux/MacOS 系统,选择对应的多路复用模型,最后剩下的系统都用 select 模型。

五、Redis的高吞吐量原因

其实 Redis 主要是通过以下个方面来满足高效吞吐量的性能需求

  • 高效的数据结构

  • 多路复用 IO 模型

  • 事件机制

  • 纯内存操作,避免频繁读写IO浪费时间

  • 单线程运行,省去了线程上下文切换带来的性能开销,效率更高




 

 

 

六、redis 持久化方式

主要有两种持久化方式RDB和AOF

RDB:全称 Redis Database,在指定的时间间隔内将内存中的数据集以快照的方式写入磁盘,实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储,在恢复数据时将快照文件直接读到内存里。

优点:

RDB 快照是压缩后的二进制文件,文件的大小会很小,比较适合使用全量复制与备份的场景。

相比于 AOF 机制,如果数据集很大,RDB 的恢复效率会更高

缺点:

如果想保证数据的高可用性,即最大限度的避免数据丢失,那么 RDB 不是一个很好的选择,因为系统一旦在定时持久化之前出现宕机现象,没有来得及写入磁盘的数据都将丢失。

由于每次生成 RDB 快照都需要 fork 子进程生成全量数据的快照,占用 CPU 与磁盘资源,不适合于频繁执行。

兼容问题,不同版本的 redis 生成的快照可能不兼容。

AOF:全称为 Append Only File,将操作命令与数据以格式化的方式追加到操作日志文件的尾部,在 append 操作返回后(已经写入到文件或者即将写入),才进行实际的数据变更,日志文件保存了历史所有的操作过程,当 redis server 需要恢复数据时,可直接重放该日志文件,即可还原所有的操作过程。

在 redis 中提供每秒同步、每次修改同步、不同步3 种同步策略。实际每秒同步是异步完成的,其效率高,一旦系统出现宕机,则这一秒内修改的数据将会丢失。

每次修改同步,即每次发生的数据变化都会被立即记录到磁盘中,可想而至,这种同步方式效率是最低的。

优点:

AOF 机制提供更高的数据安全性,即数据持久性。

AOF 持久化方式包含一个格式清晰、易于理解的日志内容,用于记录所有的修改操作。

对于写入了一半数据后出现了系统崩溃的现象,redis 能通过 redis-check-aof 工具帮助解决数据一致性的问题。

当日志文件过大时,redis 会启动 rewrite 机制,可以删除其中的某些命令。

缺点:

对相同数量的数据而言,AOF 文件通常要大于 RDB 文件,AOF 的恢复数据的速度要比 RDB 效率低。

根据同步策略的不同,AOF 在运行效率上通常会慢于 RDB,但每秒同步策略的效率是比较高的,禁用同步策略的效率和 RDB 效率类似。

对于选择哪种持久化方式,可根据系统能否接受部分性能的牺牲,通过 AOF 方式换取更高的数据一致性,或者禁用 RDB 备份换取更高的性能,待请求量或流量少的时间点再定时执行 save 命令做快照备份,但目前生产环境接触的更多都是二者结合使用的。


 

七、redis 过期数据清除机制

1、redis 内存淘汰策略(在conf文件中配置用哪一个策略)

 当前已用内存超过 redis 配置的 maxmemory 限定时,会触发主动清理策略,策略如下:

noeviction :不进行数据淘汰,当缓存被写满后,Redis不提供服务直接返回错误。

volatile-random :在设置过期时间的键值对中随机删除。

volatile-ttl :在设置过期时间的键值对,基于过期时间的先后进行删除,越早过期的越先被删除。

volatile-lru:基于LRU(Least Recently Used) 算法筛选设置了过期时间的键值对, 最近最少使用的原则筛选数据。

volatile-lfu:使用 LFU( Least Frequently Used ) 算法选择设置了过期时间的键值对, 使用频率最少的原则筛选数据

allkeys-random:从所有键值对中随机选择并删除数据。

allkeys-lru:使用 LRU 算法在所有数据中进行筛选。

allkeys-lfu:使用 LFU 算法在所有数据中进行筛选。

普通LRU与redis LRU的区别

1、普通LRU算法

LRU 算法就是指最近最少使用(Least Recently Used,LRU)算法,这是一个经典的缓存算法。

从基本原理上来说,LRU 算法会使用一个链表来维护缓存中每一个数据的访问情况,并根据数据的实时访问,调整数据在链表中的位置,然后通过数据在链表中的位置,来表示数据是最近刚访问的,还是已经有一段时间没有访问了。

一般LRU 算法的执行,可以分成三种情况来掌握:

当有新数据插入时,LRU 算法会把该数据插入到链表头部,同时把原来链表头部的数据及其之后的数据,都向尾部移动一位。

当有数据刚被访问了一次之后,LRU 算法就会把该数据从它在链表中的当前位置,移动到链表头部。同时,把从链表头部到它当前位置的其他数据,都向尾部移动一位。

当链表长度无法再容纳更多数据时,若再有新数据插入,LRU 算法就会去除链表尾部的数据,这也相当于将数据从缓存中淘汰掉。

所以我们可以发现,如果要严格按照 LRU 算法的基本原理来实现的话,我们需要在代码中实现如下内容:

要为 Redis 使用最大内存时,可容纳的所有数据维护一个链表;

每当有新数据插入或是现有数据被再次访问时,需要执行多次链表操作。

而假设 Redis 保存的数据比较多的话,那么,这两部分的代码实现,就既需要额外的内存空间来保存链表,还会在访问数据的过程中,让 Redis 受到数据移动和链表操作的开销影响,从而就会降低 Redis 访问性能。

    

     近似LRU算法非常简单,在Redis的key对象中,增加24bit用于存储最近一次访问的系统时间戳,当客户端对Redis服务端发送key的写入相关请求时,发现内存达到maxmemory,此时触发惰性删除;Redis服务通过随机采样,选择5个满足条件的key(注意这个随机采样allkeys-lru是从所有的key中随机采样,volatile-lru是从设置了过期时间的所有key中随机采样),通过key对象中记录的最近访问时间戳进行比较,淘汰掉这5个key中最旧的key;如果内存仍然不够,就继续重复这个步骤

最初Redis是这样实现的:

随机选三个Key,把idle time最大的那个Key移除。后来,把3改成可配置的一个参数,默认为N=5:maxmemory-samples 5

于是Redis又做出了改进:采用缓冲池(pooling)

当每一轮移除Key时,拿到了这个N个Key的idle time,如果它的idle time比 pool 里面的 Key的idle time还要大,就把它添加到pool里面去。这样一来,每次移除的Key并不仅仅是随机选择的N个Key里面最大的,而且还是pool里面idle time最大的,并且:pool 里面的Key是经过多轮比较筛选的,它的idle time 在概率上比随机获取的Key的idle time要大,可以这么理解:pool 里面的Key 保留了"历史经验信息"。采用"pool",把一个全局排序问题 转化成为了 局部的比较问题。(尽管排序本质上也是比较,囧)。要想知道idle time 最大的key,精确的LRU需要对全局的key的idle time排序,然后就能找出idle time最大的key了。

八、redis 部署方式

单机模式,即只有一个 redis 实例,所有的服务都连接到该实例上,该模式不适用于生产环境,若 redis 实例发生宕机或内存不足等,将导致所有服务都受影响。

哨兵模式,redis 官方推荐的高可用性方案,在 master 宕机后,redis 本身不具备自动主备切换的功能,而 redis-sentinel 是一个独立运行的进程,它能监控多个 master-slave 集群,发现 master 宕机后能自动选举新的 master。

集群模式,随着业务和数据量剧增,已达到单节点性能瓶颈,垂直扩容受机器限制,水平扩容涉及对业务的影响,及数据迁移时存在数据丢失的风险。

因此在 redis 3.0 推出 cluster 分布式集群方案,当遇到单节点内存、并发、流量瓶颈时,可采用cluster 方案实现负载均衡,该方案主要解决分片问题,把整个数据按照规则分成多个子集存储在多个不同 redis 节点上,每个节点各自负责整个数据的一部分。


 

1、主从复制

      既然是集群那么数据如何同步呢?就出现了主从复制,这个模式可以保证多台服务器的数据一致性,且主从服务器之间采用的是「读写分离」的方式。

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令进行数据同步。

 

也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段(发送新写操作命令)。

 

第一阶段中主服务器的 runID 和主服务器目前的复制进度 offset。从服务器收到响应后,会记录这两个值。

  第二阶段中主服务器生成 RDB 这个过程是不会阻塞主线程的,也就是说 Redis 依然可以正常处理命令。但是这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。那么为了保证主从服务器的数据一致性,主服务器会将在 RDB 文件生成后收到的写操作命令,写入到 replication buffer 缓冲区里。

第三阶段中在主服务器生成的 RDB 文件发送后,然后将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,然后从服务器重新执行这些操作。

 

主从服务器在完成第一次同步后,就会基于长连接进行命令传播。

 

但是在复制过程中网络断开又恢复正常了,要怎么继续保证主从服务器的数据一致性呢?

在 Redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次全量复制,很明显这样的开销太大了,必须要改进一波。

所以,从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。

 

repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;

replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。

那repl_backlog_buffer 缓冲区是什么时候写入的呢?

在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。

 

网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:

如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;

相反,如果判断出从服务器要读取的数据已经不存在

repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。

当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区我们前面也提到过,它是缓存将要传播给从服务器的命令。

repl_backlog_buffer 缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入的话,就会覆盖之前的数据。

因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。这种情况下就需要调整缓冲区的大小了。

2、代理主服务器(分摊主服务器的压力)

    在前面的分析中,我们可以知道主从服务器在第一次数据同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。

主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,就会带来两个问题:

由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;

传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响

Redis 会在从服务器中选择一个代理主服务器,我们可以把拥有从服务器的从服务器当作代理主服务器,它不仅可以接收主服务器的同步数据,自己也可以同时作为主服务器的形式将数据同步给从服务器,组织形式如下图:

 

 

 

 

 

 

 

 

 

3、主从复制启用

从节点开启主从复制,有3种方式:

  1. 配置文件: 在从服务器的配置文件中加入:slaveof <masterip> <masterport>

  2. 启动命令: redis-server启动命令后加入 --slaveof <masterip> <masterport>

  3. 客户端命令: Redis服务器启动后,直接通过客户端执行命令:slaveof <masterip>
    <masterport>,则该Redis实例成为从节点。

但主从复制架构有一个缺陷,就是当master节点发生故障后,无法自动实现故障转移,需要人手动从slave节点中选择一个作为master节点继续提供服务

九、哨兵机制

哨兵(sentinel)在Redis主从架构中是一个非常重要的组件,是在Redis2.8版本引入的。它的主要作用就是监控所有的Redis实例,并实现master节点的故障转移。哨兵是一个特殊的redis服务,它不负责数据的读写,只用来监控Redis实例。

Redis sentinel工作原理

在哨兵模式架构中,client端在首次访问Redis服务时,实际上访问的是哨兵(sentinel),sentinel会将自己监控的Redis实例的master节点信息返回给client端,client后续就会直接访问Redis的master节点,并不是每次都从哨兵处获取master节点的信息。

sentinel会实时监控所有的Redis实例是否可用,当监控到Redis的master节点发生故障后,会从剩余的slave节点中选举出一个作为新的master节点提供服务,并将新master节点的地址通知给client端,其他的slave节点会通过slaveof命令重新挂载到新的master节点下。当原来的master节点恢复后,也会作为slave节点挂在新的master节点下。如下图:

 

一般情况下,为了保证高可用,sentinel也会进行集群部署,防止单节点sentinel挂掉。当sentinel集群部署时,各sentinel除了监控redis实例外,还会彼此进行监控。

 

Redis sentinel是如何进行监控的

要实现Redis节点的监控,sentinel首先要得到所有的Redis节点的信息。sentinel通过在配置文件中配置 sentinel monitor 选项来指定要监控的redis master节点的地址,然后在启动sentinel时,会创建与redis master节点的连接并向master节点发送一个info命令,master节点在收到info命令后,会将自身节点的信息和自己下面所有的slave节点的信息返回给sentinel,sentinel收到反馈后,会与新的slave节点创建连接,接下来就会每隔10秒钟向所有的redis节点发送info命令来获取最新的redis主从结构信息。

有了redis实例的主从信息后,sentinel就会以每秒钟一次的频率向所有redis实例发送一个PING命令,而且如果sentinel是集群部署的话,每个sentinel还会以同样的频率向其他sentinel实例发送PING命令。当redis实例和sentinel实例收到PING命令后,会向sentinel返回一个有效的回复:+PONG 、-LOADING 或者 -MASTERDOWN,若返回其他的回复,或者在指定时间内(sentinel down-after-milliseconds 选项配置)没有回复,那么sentinel认为实例的回复无效。如果实例在 sentinel down-after-milliseconds 时间内未返回过一次有效的回复,那该实例就会被sentinel标记为主观下线(Subjectively Down,简称 SDOWN,指的是单个 sentinel 实例对服务节点做出的下线判断)。

当redis master节点被足够数量(sentinel monitor 选项配置,其中的quorum即为指定的sentinel数量,下面会详细介绍相关参数)的sentinel标记为主观下线后,那么master节点就会被标记为客观下线(Objectively Down,简称 ODOWN,指的是多个 sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。【一个 sentinel 可以通过向另一个 sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线】)。客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例,sentinel 在将它们判断为下线前不需要进行协商, 所以slave服务器或者其他 sentinel 永远不会达到客观下线条件。

当redis master被标记为客观下线时,每个sentinel向其他slave节点发送info命令的频率由之前的10秒钟一次变为1秒钟一次。并且会通过raft算法在sentinel中选出一个leader,由leader节点完成redis的故障转移工作。

 

Redis sentinel配置

在redis安装目录下,除了有redis本身的一个配置文件外,还有一个sentinel.conf,该文件就是sentinel的配置文件。在该文件中,主要有以下几个配置:

● port:sentinel的端口,默认为26379;

● daemonize:是否后台启动,yes表示以后台方式启动运行sentinel,默认为no;

● logfile:sentinel日志文件存放路径;

● sentinel monitor :sentinel监控的master节点的名称、地址和端口号,最后一个quorums表示至少需要多少个sentinel判定master节点故障才进行故障转移。一般配置为sentinel数量/2+1。

● sentinel down-after-milliseconds :sentinel向其他实例发送PING命令后到获得响应的超时时间,单位为毫秒;

● sentinel failover-timeout :sentinel在对master进行故障转移时的超时时间,单位毫秒;

● sentinel parallel-syncs :在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步,这个数字越小,完成故障转移所需的时间就越长;

● sentinel auth-pass :如果master节点设置了密码,则需要在这里配置master节点的密码,否则sentinel无法连接master进行监控。

补充:哨兵之间的发现是通过redis的订阅机制实现的,哨兵都配置有订阅master相同的频道

十、Redis Cluster

为什么使用redis集群?

    主从复制和哨兵模式虽然强大也有一些缺点,假设master节点的内存只有4G,那slave节点所能存储的数据上限也只能是4G,主从复制架构中是读写分离的,我们可以通过增加slave节点来扩展主从的读并发能力,但是写能力和存储能力是无法进行扩展的,就只能是master节点能够承载的上限。所以,当你只需要存储4G的数据时候的,基于主从复制和基于Sentinel的高可用架构是完全够用的。

但是如果当你面临的是海量的数据的时候呢?16G、64G、256G甚至1T呢?现在互联网的业务里面,如果你的体量足够大,我觉得是肯定会面临缓存海量缓存数据的场景的。这时就需要引入Redis Cluster集群

那什么是Redis Cluster呢?

很简单,你就可以理解为n个主从架构组合在一起对外服务。Redis Cluster要求至少需要3个master才能组成一个集群,同时每个master至少需要有一个slave节点。(为什么需要至少三个这个和哨兵一样的原理方便在master下线之后选举新的master投票)

 

这样一来,如果一个主从能够存储4G的数据,如果这个集群包含了4个主从,则整个集群就能够存储16G的数据。

我们知道,主从架构中,可以通过增加slave节点的方式来扩展读请求的并发量,那Redis Cluster中是如何做的呢?虽然每个master下都挂载了一个slave节点,但是在Redis Cluster中的读、写请求其实都是在master上完成的。slave节点只是充当了一个数据备份的角色,当master发生了宕机,就会将对应的slave节点提拔为master,来重新对外提供服务。

节点负载均衡

知道了什么是Redis Cluster,我们就可以继续下面的讨论了。

不知道你思考过一个问题没,这么多的master节点。我存储的时候,到底该选择哪个节点呢?一般这种负载均衡算法,会选择哈希算法。哈希算法是怎么做的呢?

首先就是对key计算出一个hash值,然后用哈希值对master数量进行取模。由此就可以将key负载均衡到每一个Redis节点上去。这就是简单的哈希算法的实现。

那Redis Cluster是采取的上面的哈希算法吗?答案是没有。

Redis Cluster其实采取的是类似于一致性哈希的算法来实现节点选择的。那为什么不用哈希算法来进行实例选择呢?以及为什么说是类似的呢?我们继续讨论。

因为如果此时某一台master发生了宕机,那么此时会导致Redis中所有的缓存失效。为什么是所有的?假设之前有3个master,那么之前的算法应该是 hash % 3,但是如果其中一台master宕机了,则算法就会变成 hash % 2,会影响到之前存储的所有的key。而这对缓存后面保护的DB来说,是致命的打击。

什么是一致性哈希

知道了通过传统哈希算法来实现对节点的负载均衡的弊端,我们就需要进一步了解什么是一致性哈希。

  我们上面提过哈希算法是对master实例数量来取模,而一致性哈希则是对2^32取模,也就是值的范围在[0, 2^32 -1]。一致性哈希将其范围抽象成了一个圆环,使用CRC16算法计算出来的哈希值会落到圆环上的某个地方。

然后我们的Redis实例也分布在圆环上,我们在圆环上按照顺时针的顺序找到第一个Redis实例,这样就完成了对key的节点分配。我们举个例子。

  假设我们有A、B、C三个Redis实例按照如图所示的位置分布在圆环上,此时计算出来的hash值,取模之后位置落在了位置D,那么我们按照顺时针的顺序,就能够找到我们这个key应该分配的Redis实例B。同理如果我们计算出来位置在E,那么对应选择的Redis的实例就是A。

即使这个时候Redis实例B挂了,也不会影响到实例A和C的缓存。

虚拟节点机制

但是一致性哈希也存在自身的小问题,例如当我们的Redis节点分布不均匀时,就有导致各个节点上面分配到的key不均匀。如下图所示:

 

这样就会导致B上面分布不平衡,因此引入了虚拟节点,如下图所示:

 

在圆环中,增加了对应节点的虚拟节点,然后完成了虚拟节点到真实节点的映射。假设现在计算得出了位置D,那么按照顺时针的顺序,我们找到的第一个节点就是C #1,最终数据实际还是会落在节点C上。

通过增加虚拟节点的方式,使ABC三个节点在圆环上的位置更加均匀,平均了落在每一个节点上的概率。这样一来就解决了上文提到的数据存储存在不均匀的问题了,这就是一致性哈希的虚拟节点机制。

Redis Cluster采用的什么算法

上面提到过,Redis Cluster采用的是类一致性哈希算法,之所以是类一致性哈希算法是因为它们实现的方式还略微有差别。

例如一致性哈希是对2^32取模,而Redis Cluster则是对2^14(也就是16384)取模。Redis Cluster将自己分成了16384个Slot(槽位)。通过CRC16算法计算出来的哈希值会跟16384取模,取模之后得到的值就是对应的槽位,然后每个Redis节点都会负责处理一部分的槽位,就像下表这样。

 

每个Redis实例会自己维护一份slot - Redis节点的映射关系,假设你在节点A上设置了某个key,但是这个key通过CRC16计算出来的槽位是由节点B维护的,那么就会提示你需要去节点B上进行操作。同时各client上也会保存每个key对应的节点信息,请求时如果A上面没有会先告诉client去B上面请求

 

集群扩容

当新的节点加入进来的时候,它是如何获取对应的slot的呢?

答案是通过reshard(重新分片)来实现。reshard可以将已经分配给某个节点的任意数量的slot迁移给另一个节点,在Redis内部是由redis-trib负责执行的。你可以理解为Redis其实已经封装好了所有的命令,而redis-trib则负责向获取slot的节点和被转移slot的节点发送命令来最终实现reshard。假设我们需要向集群中加入一个D节点,而此时集群内已经有A、B、C三个节点了。

此时redis-trib会向A、B、C三个节点发送迁移出槽位的请求,同时向D节点发送准备导入槽位的请求,做好准备之后A、B、C这三个源节点就开始执行迁移,将对应的slot所对应的键值对迁移至目标节点D。最后redis-trib会向集群中所有主节点发送槽位的变更信息。

故障转移

Redis Cluster中保证集群高可用的思路和实现和Redis Sentinel相似。

 

简单来说,针对A节点,某一个节点(master节点)认为A宕机了,那么此时是主观宕机。而如果集群内超过半数的节点认为A挂了, 那么此时A就会被标记为客观宕机。一旦节点A被标记为了客观宕机,集群就会开始执行故障转移。其余正常运行的master节点会进行投票选举,从A节点的slave节点中选举出一个,将其切换成新的master对外提供服务。当某个slave获得了超过半数的master节点投票,就成功当选。

 当选成功之后,新的master会执行slaveof no one来让自己停止复制A节点,使自己成为master。然后将A节点所负责处理的slot,全部转移给自己,然后就会向集群发PONG消息来广播自己的最新状态。

按照一致性哈希的思想,如果某个节点挂了,那么就会沿着那个圆环,按照顺时针的顺序找到遇到的第一个Redis实例。

而对于Redis Cluster,某个key它其实并不关心它最终要去到哪个节点,他只关心他最终落到哪个slot上,无论你节点怎么去迁移,最终还是只需要找到对应的slot,然后再找到slot关联的节点,最终就能够找到最终的Redis实例了。

简单了解gossip协议

这就是Redis Cluster各个节点之间交换数据、通信所采用的一种协议,叫做gossip。

gossip算法,意思是八卦算法,在办公室中只要一个人八卦一下,在有限的时间内,办公室内的所有人都会知道八卦消息。

算法过程:集群中的一个节点广播自身信息,部分节点收到了信息,这些节点再继续在集群中传播这个节点的信息,一段时间后整个集群中都

有了这个节点的信息。实际上是gossip大部分节点都在一直做这个操作,所以集群在一段时间后信息透明。

通信过程

  • 每一个节点有两个TCP端口:一个client访问的端口,一个节点间通信端口,通信端口号等于client访问端口加10000

  • 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。

  • 接受到ping消息的节点会用Pong消息作为响应。

Redis Cluster中,节点之间的消息类型有5种,分别是MEET、PING、PONG、FAIL和PUBLISH

使用gossip的优劣

既然Redis Cluster选择了gossip,那肯定存在一些gossip的优点,我们接下来简单梳理一下。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 




 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73544.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C# 数据类型分值类型及引用类型

一 程序中的变量与常量 程序的基本任务是&#xff1a;对数据进行处理&#xff1b; 数据分为变量(variable)与常量(literal) int age18; 变量是值可以改变&#xff0c;本质上是内存的空间&#xff0c;用来存储信息 常量的值是固定的&#xff0c;直接写出来的&#xff0c;称字面…

点击按钮,下载文件

实现文件的下载功能 1、使用a标签 直接下载仅适用于浏览器无法识别的文件。 如果是浏览器支持的文件格式&#xff0c;如html、jpg、png、pdf等&#xff0c;则不会触发文件下载&#xff0c;而是直接被浏览器解析并展示 <ahref"http://xxxxxx.rar"download>下载…

vue中的性能优化

文章目录一、Vue为什么要做性能优化二、如何做vue的性能优化1. 网络请求优化link标签项目静态资源压缩懒加载利用浏览器的缓存机制高效复用项目文件总结2. 代码优化3. 用户体验优化场景1场景2一、Vue为什么要做性能优化 性能优化的目的是使网站的加载速度更快&#xff0c;用户…

【语音处理】基于自适应差分脉冲编码调制(ADPCM)的实现研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步进步&#xff0c;matlab项目目标合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信息&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算…

音视频直播系统之 WebRTC 中的协议UDP、TCP、RTP、RTCP详解

一、UDP/TCP 如果让你自己开发一套实时互动直播系统&#xff0c;在选择网络传输协议时&#xff0c;你会选择使用UDP协议还是TCP协议 假如使用 TCP 会怎样呢&#xff1f;在极端网络情况下&#xff0c;TCP 为了传输的可靠性&#xff0c;将会进行反复重发信息的操作 在 TCP 协议…

Nagios篇之Nagios服务关联飞书实现消息告警

一、前言 通常情况下&#xff0c;我们在利用Nagios监控来做服务器监控时&#xff0c;告警是必不可少的&#xff0c;以便于运维人员能够及时发现异常&#xff0c;进而处理问题&#xff0c;所以关联Nagios就变得极为重要。 Nagios关联告警的形式很多&#xff0c;可以进行短信推送…

wpf-ListView中放置可动态调节范围的刻度尺

需求描述 某个ListView占据整个窗口&#xff0c;当窗口的宽度发生改变时&#xff0c;某一列中显示的、某一行的字符数目&#xff0c;能跟随窗口宽度变化而增减。 下面是我做完的效果&#xff1a;&#xff08;只展示窗口的一部分&#xff09; 此时是窗口缩放的极限&#xff0…

为什么全光谱台灯对孩子眼睛好呢?台灯全光谱到底是什么意思

相信大家在购买台灯时有经常看到“全光谱”、“高显色”等关键词&#xff0c;其实这指的是台灯的某方面特性&#xff0c;所谓全光谱&#xff0c;就是指光线的光谱成分完全&#xff0c;与自然光别无二致。 我们都知道&#xff0c;一束自然太阳光不是由某个单一成分构成&#xff…

Briefings in Bioinformatics2021 | 药物挖掘分子设计--生成模型综述

原文标题&#xff1a;Molecular design in drug discovery: a comprehensive review of deep generative models 论文地址&#xff1a;Molecular design in drug discovery: a comprehensive review of deep generative models | Briefings in Bioinformatics | Oxford Academ…

35_DMA基本原理

目录 DMA简介 DMA框图 STM32的DMA有一下一些特征 DMA1控制器 DMA处理 数据方向 仲裁器 DMA通道 可编程的数据量 指针增量 循环模式 存储器到存储器模式 通道传输数据量 中断 通道配置过程 DMA简介 DMA全称Direct Memroy Access, 既直接存储器访问。 DMA传输将…

重编内核导致ubuntu有线连接不出现的问题

网卡是intel的i225v 千兆网卡&#xff0c;系统为ubuntu18.0.4&#xff0c;原始内核为5.4.0-135-generic&#xff0c;但是重新编译出错&#xff0c;不知道少了什么东西&#xff0c;也没去深究&#xff0c;重新下载了5.9.0的内核&#xff1b;结果重新编译内核重启有线网卡就不能用…

2.MyBatis环境搭建

数据准备 CREATE TABLE user (id int(11) NOT NULL,username varchar(30) NOT NULL,sex varchar(1) NOT NULL,birthday varchar(10) NOT NULL,address varchar(100) NOT NULL,PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;insert into user values(1,"steven&qu…

ADI Blackfin DSP处理器-BF533的开发详解2:开发环境的搭建

软硬件开发环境的搭建 纯流程化的东西&#xff0c;没什么技术含量&#xff0c;照着做就行了。 开发板和仿真器进行物理链接&#xff0c;也就是插上JTAG头。 特别特别特别注意&#xff0c;仿真器和开发板均不上电的情况下插JTAG头&#xff0c;不要带电插JTAG头&#xff0c;你…

ORB-SLAM2 ---- Frame::GetFeaturesInArea函数

目录 1.函数用处 2.步骤 3.code 4.函数解释 4.1 函数思想 4.2 代码解释 1.函数用处 找到在 以为中心&#xff0c;半径为的圆形内且金字塔层级在的特征点。 2.步骤 Step 1 计算半径为r圆左右上下边界所在的网格列和行的id Step 2 遍历圆形区域内的所有网格&#xff0c…

代码随想录Day44|完全背包、518.零钱兑换II、377.组合总和IV

文章目录完全背包518.零钱兑换II377.组合总和IV完全背包 文章链接:代码随想录 (programmercarl.com) 背包最大重量为4。 物品为&#xff1a;如果求组合数就是外层for循环遍历物品&#xff0c;内层for遍历背包。 如果求排列数就是外层for遍历背包&#xff0c;内层for循环遍历…

Linux多线程C++版(九) 线程同步和互斥-----线程信号量

目录1.基本概念2.信号量创建和销毁3.信号量加和减操作4.代码理解信号量5.信号量实例银行账户取款----实现互斥6.信号量实例计算和取结果----实现线程同步1.基本概念 信号量从本质上是一个非负整数计数器&#xff0c;是共享资源的的数目&#xff0c;通常被用来控制对共享资源的…

[附源码]JAVA毕业设计同学录网站(系统+LW)

[附源码]JAVA毕业设计同学录网站&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#…

从源码出发剖解正则表达式

✨✨hello&#xff0c;愿意点进来的小伙伴们&#xff0c;你们好呐&#xff01; &#x1f43b;&#x1f43b;系列专栏&#xff1a;【JavaSE】 &#x1f432;&#x1f432;本篇内容&#xff1a;详解正则表达式 &#x1f42f;&#x1f42f;作者简介:一名现大二的三非编程小白&…

[ MySQL ] 使用Navicat进行MySQL数据库备份 / 还原(备份.nb3文件方式)

本文主要讲解如何用Navicat&#xff08;Navicat Premium &#xff0c;或者Navicat for mysql&#xff09;进行MySQL备份和恢复数据库。 本文主要大纲为&#xff1a;使用Navicat备份工具方式进行备份和还原&#xff0c;其中包括还原自身数据库和还原到其他目标库。 文章目录一、…

Jenkins 中 shell 脚本执行失败却不自行退出

Jenkins 中 执行 shell 脚本时&#xff0c;有时候 shell 执行失败了&#xff0c;或者判断结果是错误的&#xff0c;但是 Jenkins 执行完成后确提示成功 success 。 此时&#xff0c;可以通过条件判断来解决这个问题&#xff0c;让 Jenkins 强制退出并提示执行失败 failed 。 …