概述
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片(cluster模式)来扩展写性能。
数据类型
数据类型表如下
STRING
命令行显示如下:
LIST
命令行显示如下:
链表特点
-
双端:链表节点带有prev和next指针,获取某个节点的前置节点和后直节点的复杂度都是O(1)
-
无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点
-
带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的头节点和尾节点的复杂度为O(1)
-
带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点的数量的时间复杂度为O(1)
-
多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup,free,match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
SET
命令行显示如下:
HASH
命令行显示如下:
ZSET
命令行显示如下:
数据模型
每当我们在Redis中创建一个键值对时,至少创建了两个对象,一个对象用作键值对的键,一个对象用做键值对的值
Redis的每个对象都由一个redisObject结构标识
type
记录对象类型,这个值为字符串对象,列表对象,哈希独享,集合对象,有序集合对象其中一种
编码和底层实现
对象的ptr指向对象的底层实现,而底层实现由encoding属性决定,具体编码与底层数据模型对应关系如下图
字符串
Redis没有直接使用C语言传统的字符串表示(以空字符串结尾的字符数组,简称C字符串),而是自己构建一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作Redis的默认字符串表示
在Redis中,C字符串只会做为字符串字面量(string literal)用在一些无须对字符串值进行修改的地方,比如打印日志。当Redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。
除了用来保存数据库得字符串之外,SDS还被用作缓冲区(buffer):AOF模式中得AOF缓冲区,以及客户端状态中得输入缓冲区,都是由SDS实现的。
SDS数据模型:
每个sds.h/sdshdr结构表示一个SDS值:
SDS与C字符串区别
常数级别获取字符串长度
因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串长度,程序必须遍历整个字符串,对于遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)
和C字符串不同,SDS本身在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度为O(1)。设置和更新SDS长度的工作由SDS的API在执行时自动完成。.通过使用SDS,Redis将获取字符串长度的复杂度降低为O(1),确保了获取字符串长度的工作不会成为Redis的性能瓶颈。
杜绝缓冲区溢出
如果执行strcar(s1,“cluster”),将s1的内容修改为“Redis cluster”,但是执行之前没有为s1分配足够的内存,那么执行之后就会导致s1的数据溢出到s2的空间,导致s2保存的内容被修改。
与C字符串不同,SDS的空间分配策略完全杜绝发生缓冲区溢出的可能。当SDS API需要对SDS进行修改时,API会先检查SDS空间是否满足修改所需的要求,如果不满足,API将自动将SDS空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以不会出现缓冲区溢出的问题。
减少修改字符串带来的内存分配
- 空间预分配策略
- 惰性空间释放
空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展时,程序不仅会为SDS分配修改所必须的空间们还会为SDS分配额外未使用的空间。
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出的字节,而实使用free属性将这些字节的数量记录下来,并等待将来使用。
通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。
raw和embstr编码区别
raw和embstr都是使用redisobject结构和sdshdr结构(即SDS)来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisobject和sdshdr,而embstr编码通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisobject和sdshdr两个结构。
用embstr编码的字符串对象来保存短字符串值有以下好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降为一次。
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的需要两次。
- 因为embstr编码的字符串对象的所有数据都是保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的优势。
- embstr编码的字符串对象是只读的。如果对embstr编码的字符串对象做修改,那程序会先把对象的编码从embstr转换成raw,然后再执行修改命令。所以embstr的字符串对象执行修改命令后,一定会变成一个raw编码的字符串对象。
字典(哈希表)
dictht 是一个散列表结构,使用拉链法解决哈希冲突。
Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++。
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。
跳跃表
跳跃表由zskipListNode结构定义
数据定义
level数组
- level数组包含多个元素,每个元素包含一个指向其他节点的指针,而程序可以通过这些曾来加快访问其他节点的速度。
- 一般来说,层数越多,访问其他节点速度越快
- 每次创建一个新的跳跃表时,程序根据幂次定律(power law)随机生成一个介于1到32之间的值作为level数组的大小,这个值就是层高
前进指针
- 每个层都有一个指向表尾方向的前进指针,用于从表头方向向表尾方向访问节点
跨度
- 层的跨度用于记录两个节点之间的距离
- 两个节点之间的跨度越大,距离越远
- 指向NULL的所有前进指针的跨度为0,因为他们没有连向任何其他节点
后退指针
- 节点的后退指针用于从表尾向表头方向指向节点
- 每个节点只有一个后退指针,所以每次只能后退至前一个节点
分值和成员
- 节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分支从小到大排序
- 成员对象是一个指针,指向一个字符串对象,而字符串对象存储这一个SDS值
- 在同一个跳跃表中,每个节点保存的成员对象是唯一的,但是分值是可以相同的。
- 分值相同,节点按照成员对象字典序排列,成员对象小的排在前面,成员对象大的排在后面
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点,是有序集合的底层实现之一。
跳表特点
- 平均查找和插入时间复杂度都是O(logn)。
- 通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。
- 算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间,则跳到下一个层次继续搜索。
- 在单链表中插入的复杂度为O(n),在原始链表的基础上,每两个结点提取一个结点建立索引,我们把抽取出来的结点叫做索引层或者索引,down表示指向原始链表结点的指针。
- 最底层是原始链表,上层是一级索引,再上层是二级索引
跳表的查找
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
跳表的删除
找到目标节点后,每一层都要删除目标节点
跳表的插入
- 插入需要考虑是否插入索引,插入几层
- 随机化的方法去判断是否向上层插入索引(随机函数)
大致流程
- 首先通过上面查找的方式,找到待插入的左节点。插入的话最底层肯定是需要插入的,所以通过链表插入节点(需要考虑是否为末尾节点)
- 插入完这一层,需要考虑上一层是否插入,首先判断当前索引层级,如果大于最大值那么就停止(比如已经到最高索引层了)。否则设置一个随机数1/2的概率向上插入一层索引(因为理想状态下的就是每2个向上建一个索引节点)。
- 继续第二步的操作,直到概率退出或者索引层数大于最大索引层。
如何找到上层的待插入节点?(在哪两个节点之间插入索引)
- 如果有左、上指向的指针那么可以向左向上找到上层需要插入的节点,但是如果只有右指向和下指向的我们也可以巧妙的借助查询过程中记录下降的节点
- 因为曾经下降的节点倒序就是需要插入的节点,最底层也不例外(因为没有匹配值会下降为null结束循环)
该层是目前的最高层索引,需要继续向上建立索引应该怎么办?
- 首先跳表最初肯定是没索引的,然后慢慢添加节点才有一层、二层索引,但是如果这个节点添加的索引突破当前最高层,该怎么办呢?
- 这时候需要注意了,跳表的head需要改变了,新建一个ListNode节点作为新的head,将它的down指向老head,将这个head节点加入栈中(也就是这个节点作为下次后面要插入的节点),就比如上面的9节点如果运气够好在往上建立一层节点,会是这样的。
- 插入上层的时候注意所有节点要新建(拷贝),除了right的指向,down的指向也不能忘记,down指向上一个节点可以用一个临时节点作为前驱节点。如果层数突破当前最高层,头head节点(入口)需要改变。
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
压缩列表
压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项并且每个列表项要么是小整数值,要么是长度较短的字符串,那么就会使用压缩列表作为列表键的底层实现
压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销
列表对象保存的所有字符串对象元素的长度都小于64字节;
列表对象保存的元素数量小于512个。
压缩列表构成
- zlbytes: 4字节。记录压缩列表占用的内存字节数
- zltail:4字节。记录压缩列表表尾节点距离压缩列表起始地址有多少字节
- entryX: 长度不定,压缩列包含的各个节点
- zllen: 2字节。记录压缩列表包含的节点数量
压缩列表节点(entry)构成
- previos_entry_length: 记录压缩列表前一个字节的长度
- encoding: 记录节点保存的数据类型
- content:记录保存节点的值
连锁更新
压缩列表新增某个元素或修改某个元素时,如果空间不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
压缩列表节点的 prevlen 属性会根据前一个节点的长度进行不同的空间大小分配
如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;
优点
占用一块连续的内存空间,可以利用 CPU 缓存
而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销
缺点
不能保存过多的元素,不然查询效率就会降低。
新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了
quick_list(快速链表)
quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
虽然压缩列表是通过紧凑型的内存布局节省了内存开销,但是因为它的结构设计,如果保存的元素数量增加,或者元素变大了,压缩列表会有「连锁更新」的风险,一旦发生,会造成性能下降。
quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
list_pack
quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。
Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
entry
- encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
- data,实际存放的数据
- len,encoding+data的总长度
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题
总结
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定编码,极大的提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同编码,从而优化对象在某一场景下的使用效率。
数据淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有 6 种淘汰策略:
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
内存回收机制
redis object定义如下
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
lru
lru记录的是对象最后一次被命令程序访问的时间,占据的比特数不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。
通过对比lru时间与当前时间,可以计算某个对象的空转时间;object idletime命令可以显示该空转时间(单位是秒)。object idletime命令的一个特殊之处在于它不改变对象的lru值。
refcount
refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收:
- 当创建新对象时,refcount初始化为1;
- 当有新程序使用该对象时,refcount加1;
- 当对象不再被一个新程序使用时,refcount减1;
- 当refcount变为0时,对象占用的内存会被释放。
- Redis中被多次使用的对象(refcount>1)称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。
共享对象的具体实现
Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。
对于整数值,判断操作复杂度为O(1);
对于普通字符串,判断复杂度为O(n);
而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。
虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是09999的整数值;当Redis需要使用值为09999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。
共享对象的引用次数可以通过object refcount命令查看,如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。
持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
RDB 持久化
-
将某个时间点的所有数据都存放到硬盘上
-
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
-
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
-
如果数据量很大,保存快照的时间会很长。
AOF 持久化
将写命令添加到 AOF 文件(Append Only File)的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
redis的事务特性
- redis事务不具备原子性,持久性。保证的是一致性和隔离性
- 单独隔离操作:事务的命令都会被序列化,顺序执行,不会被其他客户端的命令打扰
- 没有隔离级别的概念
- 不能保证原子性
事务命令讲解
事务的命令一共就五条,为了方便大家记忆,我们先记住下面三条命令,:
- 开启事务:MULTI
- 执行事务:EXEC
- 取消事务:DISCARD
就像mysql中用begin开启事务、用commit结束事务一样。redis中是用multi开启事务,用exec执行命令。如果在exec之前你不想执行事务了,可以用discard取消当前事务。下面我们举例说明:
开启事务和执行事务的例子
> multi //开启事务
> set s1 aaa
> set s2 bbb
> exec //执行事务
> get s1 // 获取s1的值
"aaa"
> get s2 // 获取s2的值
"bbb"
事务为什么不支持回滚
先来汇总一下redis事务执行异常的几种情况,然后再总结事务为什么不支持回滚。
语法错误导致事务执行异常,该事务取消
在开启事务后,修改h1值为11111,h2值为2222,但h2语法错误,最终导致事务提交失败,h1、h2保留原值。
> set h1 11
> set h2 22
> multi //开启事务
> set h1 11111
> sets h2 2222 // 语法错误,下面报错
(error) ERR unknown command `sets`, with args beginning with: `h2`, `2222`,
127.0.0.1:6379> exec //执行事务,因为前面有语法错误,所以此事务取消
(error) EXECABORT Transaction discarded because of previous errors.
> get h1 // h1和h2的值并没有在事务中改变
"11"
> get h2
"22"
运行时错误(Redis类型错误)导致事务异常
在开启事务后,修改s1值为1111111,s2值为2222222,但将s2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果s1值改变、s2保留原值。
> set s1 11
> set s2 22
> multi
> set s1 1111111
> lpush s2 2222222 //此处类型错误,s2的类型是字符传,但我们把S2的类型作为List提交
> exec //提交执行事务报错。
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
> get s1 //s1的值修改成功
"1111111"
> get s2 //s1的值修改失败
"22"
以上两个例子总结出,多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。
watch命令
上面我们讲到redis是的事务是不支持回滚的,但是我们一定要让它回滚怎么办呢?这就需要用的watch命令了。
watch使用要注意:watch在mutil命令之前使用.
watch的作用是:监控一个值是否发生变化,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。最后无论是否回滚,Redis都会取消执行事务前的WATCH命令。
在事务开始前用WATCH监控a1,之后修改a1为c1111,MULTI开始事务,修改a2为c2222,执行EXEC,返回nil,说明事务回滚;查看下a2的值都没有被事务中的命令所改变。
代码如下:
//先设置2个值,我们用监控a1,然后用a2判断是否发生的回滚。
127.0.0.1:6379> set a1 1111
127.0.0.1:6379> set a2 2222
127.0.0.1:6379> watch a1 //监听a1
127.0.0.1:6379> set a1 c1111 // a1的值在监控后发生了改变。
127.0.0.1:6379> multi //开始事务
127.0.0.1:6379> set a2 c2222 //设置a2的值
127.0.0.1:6379> exec //执行事务,发生错误,事务回滚
(nil)
127.0.0.1:6379> get a2 //a2的值并没有被更改,依旧是2222
"2222"
unwatch命令
unwatch命令是取消监控,这里就不过多介绍了,下面是代码:
set k1 1111
> set k2 2222
> WATCH k1
> set k1 11 //改变k1的值
> UNWATCH //取消监控
> MULTI //开启事务
> set k1 12
> set k2 22
> exec //执行事务成功了
1) OK
2) OK
> get k1
"12"
> get k2
"22"
主从复制
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
连接过程
- 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
- 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
- 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
Sentinel
Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
sentinel网络故障修复原理
主观下线
当主服务器发生故障时,此时一个sentinel发现了故障,系统并不会马上进行failover过程(这个现象称为主观下线),它会向网络中的其他Sentinel进行确认。
客观下线
接着其他Sentinel也陆续发现故障,这个时候其中一个Sentinel就会发起投票。一定数量的哨兵(在配置文件中指定)确认Master被标记为主观下线,此时将Master标记为客观下线。
sentinel的leader选举
要想完成故障切换(将故障master剔除,并将一个slave提升为master)就必须先选举一个leader。最先发现故障的sentinel向其他哨兵发起请求成为leader,其他哨兵在没有同意别的哨兵的leader请求时,就会把票投给该sentinel。当半数以上的sentinel投票通过后就认定该sentinel为leader。接下来的故障切换有该leader完成。
master选举
leader选好后将故障master剔除,从slave中挑选一个成为master。遵照的原则如下:
- slave的优先级
- slave从master那同步的数据量,那个slave多就优先。
新Master再通过发布订阅模式通知所有sentinel更新监控主机信息。
故障的主服务器修复后将成为从服务器继续工作
集群模式
redis在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的数据。cluster模式为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。
Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。Redis Cluster集群采用了P2P的模式,完全去中心化。
特点
- 集群完全去中心化,采用多主多从;所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
- 客户端与 Redis 节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
- 每一个分区都是由一个Redis主机和多个从机组成,分片和分片之间是相互平行的。
- 每一个master节点负责维护一部分槽,以及槽所映射的键值数据;集群中每个节点都有全量的槽信息,通过槽每个node都知道具体数据存储到哪个node上。