一,底层数据结构
1,简单动态字符串(simple dynamic string,SDS)
在Redis数据库里,包含字符串值得键值对在底层都是由SDS实现的。
如:
127.0.0.1:6379> set msg hello
键msg是一个字符串对象,其底层实现是一个值为"msg"的SDS。
值也是一个字符串对象,其底层实现是一个值为"hello"的SDS。
SDS结构定义:
SDS遵循C字符串以‘\0’作为结尾,保存其的一个字节的额外空间不计入SDS的len属性里,且添加空字符串到末尾等操作都是由SDS自动完成的,遵循空字符串结尾的好处是可以重用C字符串函数库里的函数。
SDS相对于C字符串的优势:
常数复杂度获取字符串长度:
由于SDS在len属性中保存了字符串长度,因此不需要遍历字符串计算长度。
杜绝缓冲区溢出:
由于C字符串不记录自身长度,当拼接一个长度大于当前字符数组剩余空间的字符串时则会出现缓冲区溢出;
而SDS再修改前会先判断剩余空间是否能装下修改后的字符串,若不能,则会先进行扩容,其扩容规则为:
--若修改后的SDS长度,即len属性的值小于1MB,那么程序将分配和len属性同样大小的未使用空间,即属性 free的值和len相同,此时buf数组实际长度为:len + free + 1,多余的1用于保存结尾的空字符。
--若修改后的SDS长度大于等于1MB,那么程序将额外分配1MB的未使用空间。
二进制安全:
C字符以空字符作为结尾标志,若字符串中包含空字符,则它会被误以为是字符串的结尾,这限制了C字符串只能保存文本数据,不能保存如图片、视频等二进制数据;
SDS以处理二进制的方式处理buf数组中的数据,不会对其做任何限制,数据写入时什么样子,读取出来就是什么样子。
2,链表
链表在Redis中应用十分广泛,如列表的底层实现之一就是链表。
链表和链表节点结构定义:
链表:
节点:
包含三个节点的链表:
Redis链表特点:
双端:链表节点对prev和next指针;
无环:表头节点prev和表尾节点next都指向NULL;
带表头和表尾指针:通过list的head和tail指针获取表头和表尾节点时间复杂度为O(1);
长度计数器:list属性len记录节点数;
多态:节点使用void*指针保存节点值,并且可通过list的dup,free,match为节点值设置特定函数,所以链表可以保存不同类型的值;
3,字典
保存键值对,键不可以重复,类似Java中的HashMap
字典结构定义
字典:
其中ht[2]用户保存哈希表,一个用于保存数据,一个用于rehash时使用,其哈希表结构为:
这个和Java中的HashMap很像,都是一个保存Entry的数组,当hash冲突时使用链指法,其dictEntry结构为:
这个和HashMap里那个内部类Node也很像
来一个完成的字典结构图:
rehash
随着不断地操作,哈希表的键值对会逐渐增多或减少,为了维持哈希表的负载因子处在一个合理范围内,当哈希表保存的键值对太多或者太少时,程序会对哈希表进行扩展或者收缩。进行扩展的目的是为了减少hash冲突,防止链表过长导致查询效率低,收缩的话就是为了节约内存。
其中负载因子定义为:
load_factor = ht[0].used / ht[0].size
对于一个初始大小为4,包含四个键值对的哈希表来说:
load_factor = 4 / 4 = 1
当满足下面两个条件之一时,哈希表将进行扩展:
1)服务器未执行BGSAVE或BGREWRITEAOF命令,且负载因子大于等于1
2)服务器正在执行BGSAVE或BGREWRITEAOF命令,且负载因子大于等于5
当负载因子小于0.1时,程序自动开始对哈希表进行收缩操作。
渐进式rehash
rehash时需要将所有ht[0]中的键值对全部移到ht[1]中,如果ht[0]中数据量非常庞大,那么一次性将这些键全部rehash到ht[1]中的话,庞大的计算量可能导致服务器在一段时间内停止服务。因此,会分多次,渐进式的将ht[0]中的数据慢慢的rehash到ht[1]中。
渐进式rehash步骤:
1) 为ht[1]分配空间。
2) 将字典中的rehashidx置为0,表示rehash开始。
3) rehash期间,每次对字典的增删改查操作时,还会顺带将ht[0]哈希表在rehashidx索引上的所有数据rehash到ht[1]上,当此rehashidx索引上的数据rehash完成后,程序会将rehashidx的值增加1。因为此时字典会同时使用ht[0]和ht[1]两个哈希表,所以删除、查找、更新等操作会在两个哈希表上进行,先在ht[0]里查找,如果没有找到则在ht[1]中查找,其中添加操作会直接保存到ht[1]中。
4) 所有数据rehash完成后,将rehashidx值设置为-1,表示rehash操作完成。
4,跳跃表
跳跃表是一种有序数据结构,查找平均时间复杂度为O(logN),最坏为O(N),可以通过顺序性操作来批处理节点。Redis使用跳跃表作为有序集合的底层实现之一。
跳跃表结构定义
跳跃表:
跳跃表节点:
示例图:
其中level表示跳跃表内层数最大的那个节点的层数,length表示跳跃表内节点数;如上3个节点的分值分别为1.0、2.0、3.0
层:
跳跃表节点的level数据可以包含多个元素,每个元素都包含一个指向其他节点的指针,可以通过这些层来加快访问其他节点的速度,一般来说,层数越多,访问速度越快。
每创建一个跳跃表节点时,程序会随机生成一个介于1和32的值作为level数组的大小,这个大小就是高度。
前进指针:
每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。
跨度:
层的跨度,即level[i].span 用于记录两个节点之间的距离,跨度是用来计算排位(rank)的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳跃表中的排位。
后退指针:
节点的后退指针backward,用于从表尾向表头方向访问节点。
分值和成员:
分值是一个double类型的浮点数,跳跃表中所有节点都按分值的大小排序,分值相同时,按照成员排序。
节点的成员对象是一个指针,指向一个使用SDS保存的字符串对象,同一个跳跃表中,成员对象不能重复。分值可以重复,且分值相同时按成员对象字典顺序进行排序。
整数集合
整数集合时集合的底层实现之一,当一个集合只包含整数值元素且元素个数不多时将使用其作为底层实现。
结构定义
contents数组保存集合元素,按从小到大排序且不能重复。
虽然contents属性声明为int8_t,但是它并不保存任何int8_t的值,contents数组真正类型取决于encoding的值,encoding取值可以是:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64,其中的16、32、64表示每个整数占用的位数。
升级
当新加入的整数类型比所有现存的元素的类型都长时,整数集合将进行升级,将所有元素类型长度升级到新加入元素的长度。
整数集合不支持降级,一旦升上去了就降不下来了。
6,压缩列表
压缩列表是列表和哈希键的底层实现之一,当一个列表只包含少量元素,且每个元素是小整数或者短字符串时,则其将使用压缩列表作为底层实现。
压缩列表结构定义
压缩列表:
zlbytes:整个压缩列表占用内存字节数。
zltail:压缩列表表尾节点距离起始地址字节数,通过使用压缩列表起始地址指针p + zltail 就能计算出最后一个节点的地址。
zlen:压缩列表包含节点数,当这个值小于UINT16_MAX(65535)时,这个值就是节点数;当这个值等于UINT16_MAX时,需要遍历压缩列表才能计算出来。
entry:列表节点。
zlend:标记压缩列表末端。
压缩列表节点:
每个压缩列表可以保存一个字节数组或者一个整数值。
previous_entry_length:记录了前一个节点的长度,根据所记录长度大小,其内存占用大小可以是1字节或5字节,单位字节。可以通过当前节点的地址值减去这个值计算出前一个节点的地址,结合上述通过zltail计算出的最后一个节点地址值就可以实现从后向前遍历整个压缩列表。
encoding:记录节点content属性所保存数据的类型及长度。
content:保存节点值,可以是字节数组或整数。
连锁更新
前面说过previous_entry_length根据前一个节点的长度大小可以占用1字节或者5字节,当前一个节点长度小于254字节时,它占用1字节;而前一个节点长度大于等于254字节时它就占用5字节。
现考虑这么一种情况:
假设压缩列表中保存若干节点,它们的长度都介于250到253字节之间,如图:
现我们将一个长度大于254字节的新节点设置为压缩列表的头节点:
由于e1之前的previous_entry_length是1字节,不足以保存长度大于254的new节点长度,因此它会扩容至5字节,使自己的长度也大于或等于254,这样e2也就得跟着扩容了......如此直到最后一个节点。
二,对象
前面我们介绍了Redis用到的所有主要的数据结构,但Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含:字符串、列表、哈希、集合、有序集合。
Redis使用对象来表示数据库中的键和值,每当我们在Redis中创建一个键值对时,我们至少会创建两个对象,一个键对象,一个值对象;键总是一个字符串对象,而值则可以是字符串对象,列表对象,哈希对象,集合对象或者有序集合对象。
Redis中每个对象都有redisObject结构表示:
1,类型及编码
type:记录对象类型,它可以是下图中的任何一种
TYPE命令可以返回数据库键对应的值对象的类型:
127.0.0.1:6379> set msg hello
OK
127.0.0.1:6379> rpush list hello world
(integer) 2
127.0.0.1:6379> type msg
string
127.0.0.1:6379> type list
list
不同类型值对应的type输出:
encoding:记录对象使用了什么数据结构作为对象底层实现,它可以是下图中的任何一种
每种对象可以使用的编码:
可以使用OBJECT ENCODING 命令查看一个数据库键的值对象的编码:
127.0.0.1:6379> object encoding msg
"embstr"
2,对象
1,字符串对象
字符串对象的编码可以是int,raw,embstr。
int:保存的是整数值且该整数可以用long类型表示。
embstr:保存的是字符串值且长度小于等于32字节,使用SDS保存。
raw:保存字符串值且长度大于32字节,使用SDS保存。
embstr和raw区别:
raw会调用两次内存分配函数分别创建redisObject结构和sdshdr结构;而embstr只调用一次内容分配函数来分配一块连续的空间,空间依次包含redisObject结构和sdshdr结构。
编码转换:
int编码的字符串被修改成不再是整数、embstr编码的字符串执行任何修改命令都会被转换为raw
2,列表对象
列表对象的编码可以是ziplist或者linkedlist。
ziplist:列表对象保存的所有字符串元素长度小于64字节且元素数量小于512个。
linkedlist:不满足ziplist中的任一约束时。
64和512可以通过配置文件中list-max-ziplist-value和list-max-ziplist-entries修改。
结构图:
补充:上两图中保存字符串"three"的对象的完×××式为:
3,哈希对象
哈希对象的编码可以是ziplist或者hashtable。
ziplist:哈希对象保存的所有键值对的键和值的字符串长度都小于64字节且键值对数量小于512个。
hashtable:不满足ziplist中的任一约束时。
64和512可以通过配置文件中list-max-ziplist-value和list-max-ziplist-entries修改。
127.0.0.1:6379> hmset profile name Tom age 25 career Programmer
.结构图:
4,集合对象
集合对象的编码可以是intset或者hashtable。
intset:保存的所有元素都是整数值且元素数量不超过512个。
hashtable:不满足intset中任一约束时。
512可以通过配置文件中set-max-intset-entries修改。
127.0.0.1:6379> sadd Dfruits apple banana cherry
结构图:
5,有序集合对象
有序集合的编码可以是ziplist或者skiplist。
ziplist:保存的所有元素成员的长度都小于64字节且元素数量小于128个。
skiplist:不满足ziplist中任一约束时。
128和64可以铜牌配置文件中的zset-max-ziplist-entries和zset-max-ziplist-value修改。
127.0.0.1:6379> zadd price 8.5 apple 5.0 banana 6.0 cherry
结构图:
使用ziplist:
使用skiplist:
其中,zset结构中的zsl为指向跳跃表的指针,dict为指向字典的指针。
zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都是一个集合元素,节点的object属性保存了元素成员,score属性保存分值。通过跳跃表,程序可以快速的对集合进行范围操作,如zrank、zrange命令就是基于跳跃表实现的。
dict字典为有序集合创建了一个从成员到分值的映射,字典中每个键值对保存一个元素,键保存元素成员,值保存分值。通过这个字典,程序可以一O(1)的时间复杂度查到给定成员的分值,如zscore命令就是基于此特性。
zsl和dict通过指针共享相同元素的成员和分值。
三,持久化
Redis提供了两种不同的持久化方法。一个叫做快照,它可以将存在于某一时刻的所有数据都写入硬盘里。另一种叫只追加文件,它会在执行写命令时,将被执行的命令复制到硬盘里。
1,快照持久化
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。创建快照后,用户可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,也可留在本地以便重启服务器时使用。
根据配置,快照将被写入dbfilename指定的文件里,并存储在dir指定的路径上。
如果在新的快照创建完毕前,Redis、系统或硬件三者之中任一一个崩溃,Redis都将丢失上一次创建完快照之后的数据。
创建快照的方式:
客户端发送BGSAVE命令来创建快照。对于支持BGSAVE命令的平台来说,Redis会调用fork来创建一个子进程,然后由子进程负责将快照写入硬盘,父进程则继续处理命令请求。
客户端发送SAVE命令来创建快照。接到SAVE命令的Redis服务器在快照创建完毕之前不会响应任何其他命令。SAVE命令并不常用,通常只在没有足够内存执行BGSAVE时才会使用。
配置文件中进行了save配置,如 save 60 10000,那么从Redis上一次创建快照之后开始算起,当60秒内有10000次写入时,Redis就会自动触发BGSAVE命令。如果设置多个save选项,当任一一个满足时,Redis都会触发BGSAVE。
当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行SAVE命令,并在SAVE命令执行完毕后关闭服务器。
当一个Redis服务器接收到另一个Redis服务器发来的SYNC命令时,如果当前Redis服务器还没有执行BGSAVE命令或并非刚刚执行完BGSAVE命令,那么当前服务器会执行BGSAVE命令。
2,AOF持久化
AOF持久化会将被执行的写命令写到AOF文件的末尾,因此,Redis只要从头到尾执行一次AOF文件包含的所有写命令,就可以恢复数据集。
AOF可以通过在配置文件中设置 appendonly yes 选项来打开。
配置 同步频率:
appendfsync always : 每个写命令都要同步写入硬盘,这样做会严重降低Redis的速度。
appendfsync everysec :每秒执行一次同步,显示地将多个命令写入硬盘。
appendfsync no :让操作系统来决定何时进行同步。
AOF存在的问题:
随着Redis的不断运行,AOF文件体积也会不断增长,甚至可能会用完硬盘所有空间。另一个问题,如果AOF文件过大,当Redis重启后执行所有写命令将会非常耗时。
AOF重写:
用户可以发送BGREWRITEAOF命令来重写AOF文件,Redis接收到此命令后会fork一个子进程进行AOF文件重写以使其体积更小。
配置文件中配置auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 选项来自动执行BGREWRITEAOF。如auto-aof-rewrite-percentage = 100 和 auto-aof-rewrite-min-size = 64mb,并启用AOF持久化,那么当AOF文件的体积大于64mb并且AOF文件的体积比上一次重写之后的体积大了一倍,即100%时,Redis会执行BGREWRITEAOF。
四,多机数据库
1,主从模式
在关系数据库中,通常使用一个主服务器向多个从服务器发送更新,并使用从服务器来处理所有度请求。Redis也采用了同样的方式来实现自己的复制特性。
用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器,我们称被复制的服务器为主服务器,而对主服务器进行复制的被称为从服务器。
如使用:
127.0.0.1:6379> slaveof 127.0.0.1 6380
那么服务器 127.0.0.1:6379 将成为服务器127.0.0.1:6380 的从服务器, 127.0.0.1:6380将成为主服务器。
1.1 复制功能的实现
Redis的复制功能分为同步(sync/psync)和命令传播两个操作:
同步:同步操作用于将从服务器的数据库库状态更新至主服务器当前所处的状态。其中sync为老版本中的,psync从2.8版本开始,用于代替sync命令。
命令传播:同步完成后,主服务器将自己执行的写命令发送给从服务器执行。
老板本复制过程:
新老版本主要差别是在断线重连时的处理方式不同。老版本在断线重连后需要重新发送sync命令,主服务器在收到命令后将重新执行BGSAVE创建一个完整的RDB文件,如上图中所示,断线过程中,主服务器仅多增加了三条数据却需要主服务器重新执行BGSAVE,这是很不划算的。
新版本复制过程:
psync命令具有完整同步和部分重同步两种模式:
完整同步:用于初次复制情况,其步骤同sync基本一样,都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
部分重同步:用于断线后重复制情况,当断线重连后,如条件允许,主服务器仅需将断线后的写命令发送给从服务器,而无需主服务器创建完成RDB文件。
1.2 部分重同步的实现
部分重同步以一下三个部分构成:
主服务器的复制偏移量和从服务器的复制偏移量。
主服务器的复制积压缓冲区。
服务器的运行ID。
1.2.1 复制偏移量
执行复制的双方都维护了一个复制偏移量。
主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量加N。
从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量加N。
通过对比主从服务器的复制偏移量,就可以知道主从服务器是否处于一致状态。
1.2.2 复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度的先进先出队列,默认为1MB。当主服务器进行命令传播时,它不仅会将命令发送给所有从服务器,还会将命令入队到复制积压缓冲区里,并且复制积压缓冲区还会为队列中的每个字节记录相应的复制偏移量。
当从服务器重新连上主服务器时,会将自己的复制偏移量offset发送给主服务器,主服务器会根据这个偏移量来决定对从服务器执行何种同步操作:
如果offset偏移量之后的数据还存在于复制积压缓冲区里,那么执行部分重同步操作。
如果offset偏移量之后的数据已经不存在了,那么久执行完整的重同步操作。
1.2.3 服务器运行ID
每个Redis服务器都有自己的运行ID,在服务器启动时自动生成,由40个随机的十六进制字符组成。
当从服务器对主服务器进行初次复制时,主服务器会将自己的ID发送给从服务器,从服务器会将其保存起来。当从服务器断线重连时,会将这个保存的主服务器ID发送给主服务器。
如果从服务器发送的ID和主服务器自己的ID相同,那么说明从服务器断线前复制的主服务器就是当前主服务器,主服务器可以继续尝试部分重同步操作。
如果从服务器发送的ID和主服务器自己的ID不同,主服务器将对从服务器执行完整的重同步操作。
1.3 主从链
当复制需要通过互联网进行或者需要在不同的数据中心之间进行的时候,过多的从服务器可能会导致网络不可用。而Redis的主从服务器并没有什么特别的不同,所以从服务器也可以拥有自己的从服务器,像这样:
2,哨兵模式
Sentinel是Redis的高可用解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器的所有从服务器,当主服务器下线时,自动将下线主服务器下的某个从服务器升级为新的主服务器。
Sentinel启动运行主要流程:
1)根据载入的Sentinel配置文件获取被监视的主服务器信息;
2)创建连向主服务器的网络连接:对于每个被监视的主服务器,Sentinel会创建两个连向主服务器的一部网络连接
--- 一个命令连接,专门用于向主服务器发送命令,并接收回复。
--- 一个是订阅连接,专门用于订阅主服务器的sentinel:hello频道。
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过命令回复获取主服务器的当前信息,包括主服务器本身的ID及服务器角色和其下的从服务器信息。
3)根据获得的从服务器信息创建到从服务器的命令连接和订阅连接。
Sentinel以每十秒一次的频率向从服务器发送INFO命令,并从回复中获得以下信息:包括该从服务器的主服务器的IP、端口,主从服务器的连接状态,以及从服务器的ID、角色、优先级、复制偏移量并根据这些信息对保存在Sentinel中的从服务器信息进行更新。
4)以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器的sentinel:hello频道发送以下格式的命令:
其中以s_开头的是Sentinel自己的信息;m_开头的是主服务器的信息,如果发送的目的是主服务器,那么就是该主服务器的信息,如果目的是从服务器,那么就是该从服务器所在的主服务器的信息。
5)接收来自主服务器和从服务器的频道信息,上面的2,3步分别订阅了主从服务器的sentinel:hello频道,并在第4步对该频道发送了信息,也就是或每个Sentinel既可以向该频道发送信息也可以接收该频道的信息,并能从接收到的信息中获取到其他监视了相同主服务器的Sentinel的信息。
6)根据上部获得的其他Sentinel的信息与其他Sentinel建立命令连接,最终监视相同主服务器的Sentinel将形成相互连接的网络。
7)检测主观下线状态,Sentinel默认以每秒一次的频率向所有与它创建命令连接的实例(包括主从服务器及其他Sentinel)发送PING命令,并通过实例的回复来判断实例是否在线。
Sentinel配置文件中的down-after-milliseconds指定了Sentinel判断实例进入主观下线的时间长度,如果一个实例在down-after-milliseconds毫秒内连续向Sentinel返回无效回复,那么Sentinel判定此实例下线。
8)当Sentinel将一个主服务器主观下线后,它会想其他同样监视此主服务器的Sentinel进行询问。当从其他Sentinel那里接收到足够数量的已下线判断后,Sentinel就会将主服务器判定为客观下线并对其进行故障转移。
9)当一个主服务器被判定为客观下线时,监视这个主服务器的Sentinel会进行协商选出一个领头Sentinel,并由领头Sentinel执行故障转移。
10)故障转移,从已下线主服务器的所有从服务器中选出一个将其转换为主服务器;让其他所有从服务器改为复制新的主服务器;将下线的主服务器设置为新主服务器的从服务器,当它重新上线时就会成为新主服务器的从服务器。
新主服务器选择依据:
3,集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
1,节点
每个Redis服务器称之为一个节点,一个Redis集群通常由多个节点组成,通过命令:
CLUSTER MEET ip port
将指定节点加入到当前节点所在的集群中。
2,槽指派
Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个槽,数据库中的每个键都属于这16394个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。当所有的16394个槽都有节点处理时,集群处于上线状态,否则处于下线状态。
通过向节点发送命令:
CLUSTER ADDSLOTS slot ...
可以将一个或多个槽指派给节点负责。
一个节点除了会记录自己处理的槽外还会向其他节点发送自己处理的槽并且也会记录集群所有槽的指派信息。
3,执行命令
当客户端向节点发送与数据库相关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,如果键所在的槽正好就指派给了当前节点,那么节点就直接处理这个命令;如果不是,那么节点就给客户端返回一个MOVED错误,并将命令转发给正确的节点。
可以通过命令:
CLUSTER KEYSLOT key
获取指定键属于哪个槽。
4,重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会被移动到目标节点。
5,复制与故障转移
Redis中的节点分为主节点和从节点,主节点用于处理槽,从节点用于复制主节点,并在主节点下线时,代替主节点处理请求成为新的主节点,其具体步骤为:
1)从所有从节点中选择一个成为新的主节点。
2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽都指派给自己。
3)新的主节点向集群广播一条PONG消息,让其他主节点知道自己接管了下线的主节点并负责处理下线主节点原来的槽。
选举新的主节点:
1)集群的配置纪元是一个自增计数器,初始为0,集群里的某个节点开始一次故障转移操作时,纪元增加1
2)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票机会,而第一个向主节点要求投票的从节点将会获得主节点的投票。
3)当从节点发现复制的主节点下线时,它会广播一条消息,要求所有接收到消息且有投票权(正在负责处理槽)的主节点投票给自己。
4)如果一个从节点收到的投票数大于具有投票权节点总数的一半时,这个从节点就当选为新的主节点。
5)如果没有从节点收到足够多的投票,那么集群进入新的纪元,并在此选举,直到新的主节点被选出。
可以通过命令:
CLUSTER REPLICATE node_id
让接收命令的节点成为node_id所指定的节点的从节点。
五,事务
Redis通过multi、exec、watch、discard等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
1,事务的实现
一个事务由multi命令开始,由exec命令将这个事务提交给服务器执行。
1)multi命令可以将执行该命令的客户端从非事务状态切换至事务状态。
2)当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行;当处于事务状态时,服务器会根据这个客户端发来的不同命令而执行不同的操作:
如果客户端发送的命令为exec、discard、watch、multi四个命令的其中一个,那么服务器立即执行这个命令。
如果发送的是其他命令,那么服务器将这个命令放入事务队列里,然后向客户端返回queued回复。
3)当一个处于事务状态的客户端向服务器发送exec命令时,这个exec命令会被立即执行,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,并将执行结果返回给客户端。
2,watch命令的实现
watch命令是一个乐观锁,它可以在exec命令执行之前,监视任意数量的数据库键,并在exec命令执行时,检查被监视的键是否至少有一个已经被修改过了,若果是的话,服务器将拒绝执行事务,并向客户端返回返回执行失败的空回复。
每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被watch命令监视的数据库键,而字典值则是一个链表,链表中记录了所有所有监视该键的客户端。通过watched_keys字典,服务器可以清楚的知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键。
所有对数据库的修改命令,在执行之后都会对watched_keys进行检查,查看是否有被监视的键被修改,如果有,则会将客户端的REDIS_DIRTY_CAS标识打开,标识客户端的事务安全以及被破坏。
当服务器接收到exec命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。
参考:《Redis设计与实现》《Redis实战》