社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
本文整理了Redis相关的知识,方便以后查阅。更多相关文章和其他文章均收录于贝贝猫的文章目录。
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。更多关于分布式系统的文章均收录于<分布式系列文章>中。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变之后,同步改变缓存中相应的数据即可!
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
常用命令: hget,hset,hgetall 等。
hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
key=JavaUser293847
value={
“id”: 1,
“name”: “SnailClimb”,
“age”: 22,
“location”: “Wuhan, Hubei”
}
常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是列表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:sinterstore key1 key2 key3 将交集存在key1内
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
Redis里,C字符串只会作为字符串字面量用在一些无需对字符串值进行修改的地方,比如打印日志。Redis构建了 简单动态字符串(simple dynamic string,SDS)来表示字符串值。
在Redis里,包含字符串值的键值对在底层都是由SDS实现的。除此之外,SDS还被用作缓冲区:AOF缓冲区,客户端状态中的输入缓冲区
。
struct sdshdr {
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
SDS遵循C字符串以空字符结尾的管理,空字符不计算在len属性中。这样,SDS可以重用一部分C字符串函数库,如printf。
优点
Redis构建了自己的链表实现。列表键的底层实现之一就是链表。发布、订阅、慢查询、监视器都用到了链表。Redis服务器还用链表保存多个客户端的状态信息,以及构建客户端输出缓冲区。
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
typedef struct list {
listNode *head;
listNode *tail;
unsigned long len;
void *(dup)(void *ptr); // 节点复制函数
void (*free)(void *ptr); // 节点释放函数
int (*match)(void *ptr, void *key); // 节点值对比函数
} list;
特点
Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查都是构建在字典的操作之上。
字典还是哈希键的底层实现之一,但一个哈希键包含的键值对比较多,又或者键值对中的元素都是较长的字符串时,Redis就会用字典作为哈希键的底层实现。
Redis的字典使用哈希表作为底层实现,每个哈希表节点就保存了字典中的一个键值对。
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值,总是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key; // 键
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表。一次解决键冲突的问题
struct dictEntry *next;
}
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
/*
哈希表
一般情况下,字典只是用ht[0]哈希表,ht[1]只会在对ht[0]哈希表进行rehash时使用
*/
dictht ht[2];
// rehash索引,但rehash不在进行时,值为-1
// 记录了rehash的进度
int trehashidx;
} dict;
Redis计算哈希值和索引值的方法如下:
# 使用字典设置的哈希函数,计算key的哈希值
hash = dict.type.hashFucntion(key)
# 使用哈希表的sizemask属性和哈希值,计算出索引值
# 根据情况的不同,ht[x]可以使ht[0]或ht[1]
index = hash & dict.ht[x].sizemask
当字典被用作数据库或哈希键的底层实现时,使用MurmurHash2算法来计算哈希值,即使输入的键是有规律的,算法仍能有一个很好的随机分布性,计算速度也很快。
Redis使用链地址法解决键冲突,每个哈希表节点都有个next指针。
随着操作的不断执行,哈希表保存的键值对会增加或减少。为了让哈希表的负载因子维持在合理范围,需要对哈希表的大小进行扩展或收缩,即通过执行rehash(重新散列)来完成:
满足以下任一条件,程序会自动对哈希表执行扩展操作:
1、服务器目前没有执行BGSAVE或BGREWRITEAOF,且哈希表负载因子大于等于1 2、服务器正在执行BGSAVE或BGREWRITEAOF,且负载因子大于5
其中负载因子的计算公式:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
注:执行BGSAVE或BGREWRITEAOF过程中,Redis需要创建当前服务器进程的子进程,而多数操作系统都是用写时复制来优化子进程的效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间扩展哈希表,避免不避免的内存写入,节约内存。
将ht[0]中的键值对rehash到ht[1]中的操作不是一次性完成的,而是分多次渐进式的:
渐进式rehash过程中,字典会有两个哈希表,字典的增删改查会在两个哈希表上进行。
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳跃表支持平均O(logN)、最坏*O(N)*的查找,还可以通过顺序性操作来批量处理节点。
Redis使用跳跃表作为有序集合键的底层实现之一,如果有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串时,Redis使用跳跃表来实现有序集合键。
在集群节点中,跳跃表也被Redis用作内部数据结构。
typedef struct zskiplist {
struct zskiplistNode *header, *tail; # 指向跳跃表的表头结点 指向跳跃表的表尾节点
unsigned long length; # 记录跳跃表的长度, 即跳跃表目前包含节点的数量(表头结点不计入)
int leve; # 记录跳跃表内,层数最大的那个节点的层数(表头结点不计入)
} zskiplist;
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span; // 跨度
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
level:节点中用L1、L2、L3来标记节点的各个层,每个层都有两个属性:前进指针和跨度。前进指针用来访问表尾方向的其他节点,跨度记录了前进指针所指向节点和当前节点的距离(图中曲线上的数字)。
level数组可以包含多个元素,每个元素都有一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点。层数越多,访问速度就越快。每创建一个新节点的时候,根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为level数组的大小。这个大小就是层的高度。
跨度用来计算排位(rank):在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到就是目标节点的排位。
后退指针:BW,指向位于当前节点的前一个节点。只能回退到前一个节点,不可跳跃。
分值(score):节点中的1.0/2.0/3.0保存的分值,节点按照各自保存的分值从小到大排列。节点的分值可以相同。
成员对象(obj):节点中的o1/o2/o3。它指向一个字符串对象,字符串对象保存着一个SDS值。
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且数量不多时,Redis采用整数集合作为集合键的底层实现。
整数集合,可以保存int16_t、int32_t或者int64_t的整数值,且元素不重复,intset.h/intset结构表示一个整数集合:
typedef struct intset {
uint32_t encoding; // 决定contents保存的真正类型
uint32_t length;
int8_t contents[]; // 各项从小到大排序
} inset;
上图中,contents数组的大小为 sizeof(int16_t) * 5 = 80 位。
每当添加一个新元素到整数集合中,且新元素的类型比现有所有元素的类型都要长时,整数集合需要先升级(update),然后才能添加新元素:
因为引发升级的新元素长度比当前元素都大,所以它的值要么大于当前所有元素,要么就小于。前种情况放置在底层数组的末尾,后种情况放置在头部。
升级有两个好处
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度较短的字符串,那么Redis就会使用压缩列表来实现列表键。
当一个哈希键只包含少量键值对,并且每个键值对要么是小整数值,要么是长度较短的字符串,Redis就会使用压缩列表来实现哈希键。
压缩列表是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表的各组成部分:zlbytes | zltail | zllen | entry1 | entry2 | … | entryN | zlend
压缩列表的节点可以保存一个字节数组或者一个整数值。压缩节点的各个组成部分:previous_entry_length | encoding | content
previous_entry_length以字节为单位,记录前一个节点的长度。previous_entry_length 属性的长度可以是1字节或5字节:
程序可以通过指针运算,根据当前节点的起始地址来计算出前一个结点的起始地址。压缩列表的从尾向头遍历就是据此实现的。
节点的encoding记录了节点的content属性所保存的数据的类型和长度:
content 保存节点的值,可以是字节数组或整数,值的类型和长度由encoding属性决定。
因为previous_entry_length的长度限制,添加或删除节点都有可能引发「连锁更新」。在最坏的情况下,需要执行N次重分配操作,而每次空间重分配的最坏复杂度是O(N),合起来就是O(N^2)。
尽管如此,连锁更新造成性能问题的概率还是比较低的:
Redis并没有使用SDS、双端链表、字典、压缩列表、整数集合来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
通过这五种类型的对象,Redis可以在执行命令之前,根据对象的类型判断一个对象是否执行给定的命令。使用对象的好处是,可以针对不同的场景,为对象设置多种不同的数据结构的实现,从而优化使用效率。
除此之外,Redis还实现了引用计数的内存回收机制。当程序不再需要某个对象的时候,它所占用的内存会被自动释放。另外,Redis还用引用计数实现了对象共享,让多个数据库键共享同一个对象来节约内存。
最后,Redis的对象带有访问时间记录信息,空转时长较大的键可能被优先删除。
Redis使用对象来表示数据库中的键和值。创建一个新键值对时,至少会创建两个对象,一个对象用作键,一个对象用作值。每个对象都由一个redisObject结构表示:
typedef struct redisObject {
unsigned type: 4; // 类型
unsigned encoding: 4; // 编码
void *ptr; // 指向底层实现数据结构的指针
// ...
} robj;
键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
但数据库执行TYPE命令时,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
如果假设你设置了一批 key 只能存活1个小时,1小时后,redis会通过定期删除+惰性删除进行删除:
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制。
Redis支持持久化,而且支持两种不同的持久化操作。Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
创建快照的办法有如下几种:
如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。
与RDB持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会调用fsync写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync同步一次,显示地将多个写命令同步到硬盘
appendfsync no #所有数据写入操作系统的文件缓存,让操作系统决定何时进行同步
appendfsync always 可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。不过,我个人觉得可以将redis服务器连上带写缓存和电源保护的RAID,这样AOF文件顺序写入也很快,而且每次都是写入到RAID的缓存中,并不是实际写盘,电池保护也保证了数据的持久性。
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户一般只会丢失一秒之内产生的数据,但如果redis的写入压力很大时,最多还会多写入1秒的数据量(相对于AOF文件缓冲区),所以理论上最多会丢失2秒的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
appendfsync no 选项一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。
虽然AOF持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求,但AOF持久化也有缺陷——AOF文件的体积太大。
AOF虽然在某个角度可以将数据丢失降低到最小而且对性能影响也很小,但是极端的情况下,体积不断增大的AOF文件很可能会用完硬盘空间。另外,如果AOF体积过大,那么还原操作执行时间就可能会非常长。
为了解决AOF体积过大的问题,用户可以向Redis发送 BGREWRITEAOF命令 ,这个命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件来减小AOF文件的体积。BGREWRITEAOF命令和BGSAVE创建快照原理十分相似,所以AOF文件重写也需要用到子进程,这样会导致性能问题和内存占用问题,和快照持久化一样。更糟糕的是,如果不加以控制的话,AOF文件的体积可能会比快照文件大好几倍。
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的,RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
watch key1 key2
multi
set key1 value1
set key2 value2
exec # 如果key1或者key2被其他人修改了那么上述的两条命令就不会执行,redis借此来实现事务机制
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
Redis中,用户可以执行SAVEOF命令或设置saveof选项,让一个服务器去复制(replicate)另一个服务器。被复制的服务器叫做master,对master进行复制的服务器叫做slave。
进行复制中的master和slave应该保存相同的数据,这称作“数据库状态一致”。
复制开始时,slave会先执行同步操作,步骤如下:
同步完成后,主从服务器的一致状态仍有可能改变,每当master执行写命令时,主从服务器的状态就会不一致。为此,master执行写命令,并将其发送给slave一并执行。
Redis的复制可以分为两种情况:
对于初次复制,旧版复制功能可以很好完成。但是断线后复制,效率却很低,因为重连后会浪费一次SYNC操作。
为了解决旧版复制功能在断线后的低效问题,Redis从2.8之后,使用PSYNC代替SYNC执行复制时的同步操作。PSYNC具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
master和slave分别维护一个复制偏移量:
对比两者的复制偏移量,就知道它们是否处于一致状态。
复制积压缓冲区是master维护的一个固定长度的FIFO队列,默认大小为1MB。当服务器进行命令传播时,不仅会将命令发送给所有slave,还会入队到积压缓冲区。因此,积压缓冲区保存了最近被传播的写命令,且为队列中的每个字节记录相应的复制偏移量。
slave重连上master时,slave通过PSYNC将自己的复制偏移量offset发送给master,master会根据这个offset决定slave执行何种同步操作:
部分重同步还要用到服务器运行ID,主从服务器都有自己的ID。初次复制时,master将自己的ID传给slave,后者将其保存。
断线重连后,slave向当前连接的master发送之前保存的ID:
PSYNC的调用方式有两种:
runid
offset
命令,接收到这个命令的master会根据runid和offset来判断执行哪种同步。port-number
,向master发送slave的监听端口号。master收到后,会将端口号放到客户端状态的slave_listening_port属性中该属性的唯一作用就是master执行INFO replication命令时打印slave的端口号。Sentinel(哨兵)是Redis的高可用性解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个master以及属下的所有slave。Sentinel在被监视的master下线后,自动将其属下的某个slave升级为新的master,然后由新的master继续处理命令请求。
当一个Sentinel启动时,会执行以下几步:
连接建立后,Sentinel将成为master的客户端,可以向其发送命令。对于被监视的master来说,Sentinel会创建两个异步网络连接:
__sentinel__:hello
频道。Sentinel以默认10秒一次的频率,向master发送INFO命令,获取其当前信息:
Sentinel发现master有新的slave时,除了会为这个slave创建相应的实例结构外,还会创建到它的命令连接和订阅连接。
通过命令连接,Sentinel会向slave每10秒发送一次INFO命令,根据回复更新slave的实例结构:
默认情况下,Sentinel会以两秒一次的频率,通过命令连接向所有被监视的master和slave发送:
PUBLISH __sentinel__:hello "<s_ip>, <s_port>, <s_runid>, <s_epoch>, <m_name>, <m_ip>, <m_port>, <m_epoch>"
其中以s_开头的参数表示Sentinel本身的信息,m_开头的参数是 master 的信息。如果Sentinel 正在监视的是 slave,那就是 slave 正在复制的 master 信息。
当Sentinel与一个master或slave建立订阅连接后,会向服务器发送以下命令:SUBSCRIBE __sentinel__:hello
Sentinel对__sentinel__:hello
频道的订阅会持续到两者的连接断开为止。也就是说,Sentinel既可以向服务器的__sentinel__:hello
频道发送信息,又通过订阅连接从__sentinel__:hello
频道接收信息。
对于监视同一个server的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel收到。这些信息用于更新其他Sentinel对发送信息Sentinel和被监视Server的认知。
Sentinel为master创建的实例结构中,有sentinels字典保存了其他监视这个master的Sentinel:
当一个Sentinel收到其他Sentinel发来的信息时,目标Sentinel会从信息中提取出:
根据提取的参数,目标Sentinel会在自己的Sentinel状态中更新sentinels和masters字典。
Sentinel通过频道信息发现一个新的Sentinel时,不仅会为其创建新的实例结构,还会创建一个连向新Sentinel的命令连接,新的Sentinel也会创建连向这个Sentinel的命令连接,最终,监视同一master的多个Sentinel成为相互连接的网络。各个Sentinel可以通过发送命令请求来交换信息。
默认情况下,Sentinel会每秒一次地向所有与它创建了命令连接的实例(master、slave、其他sentinel)发送PING命令,并通过回复来判断其是否在线。只有+PONG/-LOADING/-MASERDOWN三种有效回复。
Sentinel的配置文件中down-after-milliseconds选项指定了判断实例主观下线所需的时间长度。在down-after-milliseconds毫秒内,如果连续返回无效回复,那么Sentinel会修改这个实例对应的实例结构,将flags属性中打开SRI_S_DOWN标识,标识主观下线。
当Sentinel将一个master判断为主观下线后,为了确认是真的下线,会向监视这一master的其他Sentinel询问。有足够数量(quorum)的已下线判断后,Sentinel会将master判定为客观下线,并对master执行故障转移。
master被判定为客观下线后,监视这个master的所有Sentinel会进行协商,选举一个领头Sentinel,并由其对该master执行故障转移。选举的规则如下:
领头Sentinel会对已下线的master执行故障转移,包括以下三个步骤:
新master的挑选规则:
Redis集群是分布式的数据库方案,通过分片(sharding)来进行数据共享,并提供复制或故障转移功能。
一个节点就是运行在集群模式下的Redis服务器,根据cluster-endabled配置选项是否为yes来决定是否开启集群模式。
节点在集群模式下会继续使用单机模式的组件,如:
通过向节点A发送CLUSTER MEET命令,客户端可以让接受命令的节点A将另一个节点B接入到A所在的集群中。
收到CLUSTER MEET命令的节点A,会进行以下操作:
Redis集群通过分片的方式保存数据库中的键值对:集群中的整个数据库被分为16384个槽(slot),数据库中的每个键都属于其中的一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok),如果任何一个槽都没有得到处理,就处于下线状态(fail)。
CLUSTER MEET只是将节点连接起来,集群仍处于下线状态,通过向节点发送CLUSTER ADDSLOTS,可以为一个或多个槽指派(assign)给节点负责。
struct clusterNode {
unsigned char slots[16384/8];
int numslots;
};
slots数组中的索引i上的二进制位的值来判断节点是否负责处理槽i。numslots记录节点负责处理的槽的数量,即slots数组中二进制1的数量。
一个节点除了会将自己处理的槽记录在clusterNode结构中的slots和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其它节点。
节点A通过消息从节点B接收到节点B的slots数组会,会在自己的clusterState.nodes字典中查找节点B对应的clusterNode结构,并对结构中的slots数组进行更新。
最终,集群中的每个节点都知道数据库中的16384个槽分别被指派给了哪些节点。
客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的键属于哪个槽,并检查这个槽是否被指派给了自己:
如果指派给了自己,节点直接执行命令。否则,节点向客户端返回一个MOVED错误,指引客户端转向(redirect)到正确的节点,再次发送命令。
计算槽:
def slot_number(key):
return CRC16(key) & 16383
节点计算出键所属的槽i之后,会检查自己在clusterState.slots数组中的第i项,判断键所在的槽是不是自己负责。
如果不是由自己负责,它会通过一个从slot->node的映射(跳表实现)来找到负责该槽的节点,并发送一个MOVED回应来通知客户端应该去找哪个节点。
Redis集群的重新分片指的是将任意数量已经指派给某个节点的槽改为指派给另一个节点,且相关槽所属的键也从源节点移动到目标节点。重新分片可以在线(online)进行,分片过程中,集群不需要下线,且源节点和目标节点都可以继续处理命令请求。
重新分片是由Redis的集群管理软件redis-trib负责的,Redis提供了重新分片所需的所有命令,redis-trib则通过向源节点和目标节点发送命令来实现重新分片:
slot
IMPORTING source_id
命令,让目标节点准备好导入源节点中属于槽slot的键值对。slot
MIGRATING target_id
命令,让源节点准备好迁移键值对。slot
count
命令,获得最多count个属于槽slot的键值对的键名。target_ip
target_port
key_name
0 timeout
命令,将选中的键原子地从源节点迁移到目标节点。slot
NODE target_id
命令,将槽slot指派给目标节点,这一指派信息通过消息传送至整个集群。
在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现:属于被迁移槽的一部分键值对保存在源节点中,而另一部分保存在目标节点中。
当客户端向源节点发送一个与数据库键有关的命令,且要处理的键恰好就属于正在被迁移的槽时:
节点收到一个关于键key的命
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!