赞
踩
Redis 没有直接使用 C 语言传统的字符吕表示 (以空字符结尾的字符数组,以下简称 C 字符串),而是自己构建了 一种名为简单动态字符串(simple dynamic string,SDS)的抽象象类型,并将 SDS 用作 Redis 的默认字符串表示。
存储 String 类型的 key-value 时,key 和 value 都是 SDS 类型的.字符串键值都用 SDS 表示.
redis> SET msg "hello world"
OK
**当做缓冲区使用:**除了用来保存数据库中的字符串值之外,SDS 还被用作缓冲区(buffer)。AOF 模块中的 AOF 缓冲区,以及客户端状态中的输入缓冲区,都是由 SDS 实现的
struct sdshdr{
//字节数组
char buf[];
//buf数组中已使用字节数量
int len;
//buf数组中未使用字节数量
int free;
}
根据传统,C 语言使用长度为 N+1 的字符数组来来表示长度为 N 的字符串,并且字符数组的最后一个元素总是空字符’\0’。
SDS 优点:
空间预分配用于优化 SDS 的字符串增长操作:当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须要的空间,还会为 SDS 分配额外的未使用空间。
其中,额外分配的未使用空间数量由以下公式决定:
通过空间预分配策略,Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。需要注意的是字符串最大长度为 512M。
惰性空间释放用于优化 SDS 的字符串缩短操作:当 SDS 的 API 需要缩短 SDS 保存的字符串时时,程序并不立即使用内内存重分配来回收缩短后多出来的字节,而是是使用 free 属性将这些字 2 节的数量记录起来,并等待将来使用。
通过惰性空间释放策略,SDS 避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。
与此同时,SDS 也提供了相应的 API,让我们可以在有需要时,真正地释放 SDS 的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。
链表的优点
如上图所示,Redis 链表由一个 list 和任意个 listNode 结构组成;
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;
双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)。
无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL 对链表的访问以 NULL 为终点。
带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)。
带链表长度计数器:程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)。
多态:链表节点使用 void*指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值),这些关联的键和值就称为键值对。
字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对。
字典用哈希表做底层实现,哈希表由哈希表节点产生,每个节点保存了字典的键值对。
typedef struct dict { //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; //rehash 索引。当rehash 不在进行时,值为-1 in trehashidx; /* rehashing not in progress if rehashidx == -1 */ } dict; typedef struct dictType { //计算哈希值的函数 unsigned int (*hashFunction)(const void *key); //复制键的函数 void *(*keyDup)(void *privdata, const void *key); //复制值的函数 void *(*valDup)(void *privdata, const void *obj); //对比键的函数 int (*keyCompare) (void *privdata, const void *key1, const void *key2); //销毁键的函数 void (*keyDestructor)(void *privdata, void *key); //销毁值的函数 void (*valDestructor)(void *privdata, void *obj); } dictType;
成员说明
hash 算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用 MurmurHash2 算法来计算键的哈希值。
发生冲突时(键被分到哈信表数组同一个索引上),用拉链法解决,next 指向,新接待那添加到链表表头(O (1))
#使用字典设置的哈希函数,计算键key 的哈希值hash = dict->type->hashFunction(key);
#使用哈希表的sizemask 属性和哈希值,计算出索引值
#根据情况不同,ht[x] 可以是ht[0] 或者ht[1]
index = hash & dict->ht[x].sizemask;
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值。总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
成员介绍:
table 指向 dictEntry(哈希表节点,保存键值对),size 记录大小,sizemask 则是 size-,用于(n-1&hash)运算放到 table 的索引。
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(loadfactor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行 rehash(重新散列)操作来完成,Redis 对字典的哈希表执行 rehash 的步骤如下:
rehash 为渐进式,分多次渐进完成扩展收缩,因为如果键值对数量庞大,全部一次性 rehash 会停止服务,通过 rehashidx 来实现.
因为在进行渐进式 rehash 的过程中,字典会同时使用 ht[0]和 ht[1]两个哈希表,所以在渐进式 rehash 进行期间,字典的删除(delete)、查找(fnd)、更新(update)等操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会先在 ht[0]里面进行查找,如果没找到的话,就会继续到 ht[1]里面进行查找。诸如此类。
另外,在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1]里面,而 ht[0]则不再进行任何添加操作,这一措施保证了 ht[0]包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
//负载因子的计算
load_factor=ht[0].used/ht[0].size
整数集合是集合键的底层实现之一。
整数集合的底层实现为数组, 这个数组以有序、无重复的方式保存集合元素, 在有需要时, 程序会根据新添加元素的类型, 改变这个数组的类型。
升级操作为整数集合带来了操作上的灵活性, 并且尽可能地节约了内存。
整数集合只支持升级操作, 不支持降级操作。
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。不会出现重复元素.
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度。
虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值:
如果 encoding 属性的值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t 类型的数组,数组里的每个项都是一个 int16_t 类型的整数值(最小值为-32768,最大值为 32767)。
如果 encoding 属性的值为 INTSET_ENC_INT32,那么 contents 就是一个 int32_t 类型的数组,数组里的每个项都是一个 int32_t 类型的整数值(最小值为-2147483648,最大值为 2147483647)。
如果 encoding 属性的值为 INTSET_ENC_INT64,那么 contents 就是一个 int64_t 类型的数组,数组里的每个项都是一个 int64_t 类型的整数值(最小值为-9223372036854775808,最大值为 9223372036854775807)。
每当我们要将一个新元素添加到整数集合里面, 并且新元素的类型比整数集合现有所有元素的类型都要长时, 整数集合需要先进行升级(upgrade), 然后才能将新元素添加到整数集合里面。
升级整数集合并添加新元素共分为三步进行:
根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
将新元素添加到底层数组里面。
整数集合不支持降级操作, 一旦对数组进行了升级, 编码就会一直保持升级后的状态。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis 就会使用跳跃表来作为有序集合键的底层实现。
跳跃表的应用
下图展示了一个跳跃表示例,位于图片最左边的是 zskiplist 结构,该结构包含以下属性:
typedef struct zskiplist {
//head 指向跳跃表的表头节点,tail 指向跳跃表的表尾节点
struct zskiplistNode *header, *tail;
//记录跳跃表的长度,即跳跃表目前包含节点的数量,不包含头结点
unsigned long length;
//记录目前跳跃表内,层数最大的那个节点的层数
int level;
} zskiplist;
位于 zskiplist 结构右方的是四个 zskiplistNode 结构,该结构包含以下属性
typedef struct zskiplistNode {
//成员对象(o1,o2,o3) 是一个指针,指向sds值
sds ele;
//分值,节点按各自所保存的分值从小到大排列 double类型
double score;
//后退指针在程序从表尾向表头遍历时使用 只有一个
struct zskiplistNode *backward;
//层数组 (每一层)
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned long span;
} level[];
} zskiplistNode;
初看上去,很容易以为跨度和遍历操作有关,但实际上并不是这样,遍历操作只使用前进指针就可以完成了,跨度实际上是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位.注意不要和分值 score 弄混淆了.
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。
压缩列表(ziplist)是列表和哈希的底层实现之一。
当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表的底层实现。
当一个哈希只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做哈希的底层实现。
压缩列表是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结枃。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,如下图。
每个压缩列表节点可以保存一个字节数组或者一个整数值。其中,字节数组可以是以下三种长度中的一种。
长度小于等于 63(2^6-1)字节的字节数组;
长度小于等于 16383(2^14-1)字节的字节数组
长度小于等于 4294967295(2^32-1)字节的字节数组
整数值可以是以下 6 种长度中的一种
4 位长,介于 0 至 12 之间的无符号整数
1 字节长的有符号整数
3 字节长的有符号整数
int16_t 类型整数
int32_t 类型整数
int64_t 类型整数
节点的 previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。 previous_entry_length 属性的长度可以是 1 字节或者 5 字节。
如果前一节点的长度小于 254 字节,那么 previous_entry_length 属性的长度为 1 字节,前一节点的长度就保存在这一个字节里面。
如果前一节点的长度大于等于 254 字节,那么 previous_entry_length 属性的长度为 5 字节:其中属性的第一字节会被设置为 0xFE(十进制值 254),而之后的四个字节则用于保存前一节点的长度.
节点的encoding属性记录了节点的 content 属性所保存数据的类型以及长度。
节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的 encoding 属性决定。
多米诺牌的效应
Redis 将这种在特殊情况下产生的连续多次空间扩展操作称之为连锁更新”(cascade update)
要注意的是,尽管连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的:
因为以上原因,ziplistPush 等命令的平均复杂度仅为 O(N),在实际中,我们可以放心地使用这些函数,而不必担心连锁更新会影响压缩列表的性能。
因为 ziplistPush、ziplistlnsert、ziplistDelete 和 ziplistDeleteRange 四个函数都有可能会引发连锁更新,所以它们的最坏复杂度都是 O(N2) 。
虽说如此,Redis 针对压缩列表在设计上的不足,在后来的版本中,新增设计了两种数据结构:quicklist(Redis 3.2 引入) 和 listpack(Redis 5.0 引入)。这两种数据结构的设计目标,就是尽可能地保持压缩列表节省内存的优势,同时解决压缩列表的「连锁更新」的问题。
typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned long len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ } quicklist; typedef struct quicklistIter { const quicklist *quicklist; quicklistNode *current; unsigned char *zi; long offset; /* offset in current ziplist */ int direction; } quicklistIter;
可以看出:
quicklistNode 就是一个 ziplist,同时加入 prev、next 前后指针将其串联到 quicklist 中
quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。
因为 quicklistNode 还是用了压缩列表来保存元素,压缩列表连锁更新的问题,来源于它的结构设计,所以要想彻底解决这个问题,需要设计一个新的数据结构。
于是,Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题
Redis 用到的所有主要数据结构,比如简单动态字符串(SDS)、双端链表、字典、跳跃表,压缩列表、整数集合等等。Redis 并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。
通过这五种不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
除此之外,Redis 的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis 还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,Redis 的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了 MaxMemory 功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
其中常见的键值类型包括 String、List、Hash、Set 和 Sorted Set 这这五种,同时还支持 BitMap、HyperLogLog、Geo 和 Stream 这四种扩展类型
typedef struct redisObject {
// 类型 0-string 1-list 2-set 3-zset 4-hash,在宏中定义。
unsigned type:4; // :4 是位域(位段),表示只占用4bit,2^4
// 对象编码。某些类型的对象(如字符串和哈希)可以通过多种方式在内部表示。ENCODING表明表示方式。
unsigned encoding:4;
//LRU_BITS=24,共24位,高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)
unsigned lru:LRU_BITS;
// 引用次数
int refcount;
// 指针指向具体数据,void * 类型,从而可以执行那六大数据结构
void *ptr;
} robj;
type:type 字段表示对象的类型,占 4 个比特;当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型;
encoding表示对象的内部编码,占 4 个比特。对于 redis 支持的每种类型都至少有两种编码,对于字符串有 int、embstr、row 三种通过 encoding 属性,redis 可以根据不同的使用场景来对对象使用不同的编码,大大提高的 redis 的灵活性和效率。
ptr:指针指向对象的底层实现的数据结构.
以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,
Redis 倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。3.2 版本以后都采用 quicklist, 是压缩链表和双端链表的结合.
type
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
encoding
#define OBJ_ENCODING_RAW 0 // 编码为字符串 c语言类型
#define OBJ_ENCODING_INT 1 // 编码为整数
#define OBJ_ENCODING_HT 2 // 编码为哈希表
#define OBJ_ENCODING_ZIPMAP 3 // 编码为 zipmap
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 // 编码为压缩列表
#define OBJ_ENCODING_INTSET 6 // 编码为整数集合
#define OBJ_ENCODING_SKIPLIST 7 // 编码为跳跃表
#define OBJ_ENCODING_EMBSTR 8 // 编码为SDS字符串
#define OBJ_ENCODING_QUICKLIST 9 /* 快速列表 压缩列表+链表 */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
redisObject 使用到的底层数据结构 encoding:
encoding 属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xr11gHEn-1672365181682)(http://qinyingjie.top/blogImg/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxNDUzMjg1,size_16,color_FFFFFF,t_70-20220926104214792.png)]
OBJECT ENCODING 命令
字符吕对象的编码可以是 int、raw 或者 embstr。
embstr 编码的字符串对象在执行命令时,产生的效果和 raw 编码的字符串对象执行命令时产生的效果是相同的,但使用 embstr 编码的字符串对象来保存短字符串值有以下好处:
int 转 raw,本来是 int 类型,使用了 append 方法,会转换为 raw 类型.
embstr 没有修改的 api,默认是只读的.当修改时,先转为 raw 再进行修改.
列表对象的编码可以是 ziplist 或者 linkedlist。
ziplist 编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
linkedlist 编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用 ziplist 编码:
列表对象保存的所有字符串元素的长度都小于 64 字节;
列表对象保存的元素数量小于 512 个;不能满足这两个条件的列表对象需要使用 linkedlist 编码。
以上 2 个值是可以修改配置的.
哈希对象的编码可以是 ziplist 或者 hashtable
ziplist 编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
字典的每个键都是一个字符串对象,对象中保存了键值对的键;
字典的每个值都是一个字符串对象,对象中保存了键值对的值。
编码转换
当哈希对象可以同时满足以下两个条件时,哈希对象使用 ziplist 编码:
集合对象的编码可以是 intset 或者 hashtable。
127.0.0.1:6379> sadd numbers 1 3 5
(integer) 3
127.0.0.1:6379> sadd fruits apple cherry
(integer) 2
整数集合
hashtable
编码转换
当集合对象可以同时满足以下两个条件时,对象使用 intset 编码:
集合对象保存的所有元素都是整数值;
集合对象保存的元素数量不超过 512 个。
有序集合的编码可以是 ziplist 或者 skiplist。
ziplist
ziplist 编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score).
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
skiplist
typedef struct zset {
dict *dict; //缓存
zskiplist *zsl; //排序结构
} zset;
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个 double 类型的浮点数。值得一提是,虽然 zset 结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
编码转换
当有序集合对象可以以同时满足以下两个条件中时,对象使用 ziplist 编码:
有序集合保存的元素数量小于 128 个;
有序集合保存的所有元素成员的长度都小于 64 字节;
为什么有序集合需要同时使用跳跃表和字典来实现
类型检查通过 type 命令来实现的
Redis 中用于操作键的命令基本上可以分为两种类型。
其中一种命令可以对任何类型的键执行,
而另一种命令只能对特定类型的键执行,比如说:
SET、GET、APPEND、STRLEN 等命令只能对字符串键执行;
HDEL、HSET、HGET、HLEN 等命令只能对哈希键执行;
RPUSH、LPOP、LINSERT、LLEN 等命令只能对列表键执行;
SADD、SPOP、SINTER、SCARD 等命令只能对集合键执行;
ZADD、ZCARD、ZRANK、ZSCORE 等命令只能对有序集合键执行;
多态命令,自动选择可执行的命令.内部自己做筛选.
通过引用计数法来进行内存回收,底层是通过 redisObject 结构的 refcount 属性记录每个对象的引用计数信息;
对象的引用计数信息会随着对象的使用状态而不断变化:
在创建一个新对象时,引用计数的值会被初始化为 1;
当对象被一个新程序使用时,它的引用计数值会被增一;
当对象不再被一个程序使用时,它的引用计数值会被减一;
当对象的引用计数值至变为 0 时,对象所占用的内存会被释放。
typedef struct redisObject {
// ...
//引用计数
int refcount;
// ...
} robj;
**概念:**除了用于实现引用计数内存回收机制之外,对象的引用计数属性(refcount 属性)还带有对象共享的作用
值对象共享 整数型对象,Redis 只对字符串对象进行共享,并且只对包含整数值的字符串对象进行共享
在 Redis 中,让多个键共享同一个值对象需要执行以下两个步骤:
只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象,而一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的 CPU 时间也会越多:
如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为 O(1);
如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为 O(N);
如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,那么验证操作的复杂度将会是 O(N2)
因此,尽管共享更复杂的对象可以节约更多的内存,但受到 CPU 时间的限制,Redis 只对包含整数值的字符串对象进行共享。
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
redisObject 结构包含了属性为 Iru 属性,该属性记录了对象最后一次被命令程序访问的时间:
OBJECT IDLETIME 命令可以打印出给定键白的空转时长,这一空转时长就是通过将当前时间减去键的值对象的 Iru 时间计算得出的:
127.0.0.1:6379> OBJECT IDLETIME msg
(integer) 295277
127.0.0.1:6379> OBJECT IDLETIME price
(integer) 10747
除了可以被 OBJECT IDLETIME 命令打印出来之外,键的空转时长还有另外一项作用:如果服务器打开了 max_memory 选项,并且服务器用于回收内存的算法为 volatile-Iru 或者 allkeys-Iru,那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
常见的数据结构以及与之对应的键值类型如下
BitMap 和布隆过滤器比较
HyperLogLog
统计注册 IP 数
统计每日访问 IP 数
统计页面实时 UV 数
统计在线用户数
统计用户每天搜索不同词条的个数
说明:基数不大,数据量不大就用不上,会有点大材小用浪费空间
有局限性,就是只能统计基数数量,而没办法去知道具体的内容是什么
和 bitmap 相比,属于两种特定统计情况,简单来说,HyperLogLog 去重比 bitmap 方便很多
一般可以 bitmap 和 hyperloglog 配合使用,bitmap 标识哪些用户活跃,hyperloglog 计数
Redis 集成的 HyperLogLog 使用语法主要有 pfadd 和 pfcount,顾名思义,一个是来添加数据,一个是来统计的.
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
操作命令
适用场景
Redis 数据库中的每个键值对的键和值都是一个对象。
Redis 共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。
Redis 的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
Redis 会共享值为 0 到 9999 的字符串对象。
对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。
Redis(全称:Remote Dictionary Server 远程字典服务)
struct redisServer { /* General */ //配置文件路径 char *configfile; /* Absolute config file path, or NULL */ //serverCron()调用频率 int hz; /* serverCron() calls frequency in hertz */ //数据库对象数组指针 redisDb *db; //支持的命令列表 dict *commands; /* Command table */ //没有转化的命令 dict *orig_commands; /* Command table before command renaming. */ //事件 aeEventLoop *el; //每分钟增加一次 unsigned lruclock:22; /* Clock incrementing every minute, for LRU */ unsigned lruclock_padding:10; int shutdown_asap; /* SHUTDOWN needed ASAP */ int activerehashing; /* Incremental rehash in serverCron() */ //验证密码 char *requirepass; /* Pass for AUTH command, or NULL */ char *pidfile; /* PID file path */ int arch_bits; /* 32 or 64 depending on sizeof(long) */ int cronloops; /* Number of times the cron function run */ char runid[REDIS_RUN_ID_SIZE+1]; /* ID always different at every exec. */ int sentinel_mode; /* True if this instance is a Sentinel. */ /* Networking */ int port; /* TCP listening port */ int tcp_backlog; /* TCP listen() backlog */ char *bindaddr[REDIS_BINDADDR_MAX]; /* Addresses we should bind to */ int bindaddr_count; /* Number of addresses in server.bindaddr[] */ char *unixsocket; /* UNIX socket path */ mode_t unixsocketperm; /* UNIX socket permission */ int ipfd[REDIS_BINDADDR_MAX]; /* TCP socket file descriptors */ int ipfd_count; /* Used slots in ipfd[] */ int sofd; /* Unix socket file descriptor */ int cfd[REDIS_BINDADDR_MAX];/* Cluster bus listening socket */ int cfd_count; /* Used slots in cfd[] */ // 连接的客户端 list *clients; /* List of active clients */ list *clients_to_close; /* Clients to close asynchronously */ list *slaves, *monitors; /* List of slaves and MONITORs */ redisClient *current_client; /* Current client, only used on crash report */ int clients_paused; /* True if clients are currently paused */ mstime_t clients_pause_end_time; /* Time when we undo clients_paused */ char neterr[ANET_ERR_LEN]; /* Error buffer for anet.c */ dict *migrate_cached_sockets;/* MIGRATE cached sockets */ /* RDB / AOF loading information */ int loading; /* We are loading data from disk if true */ off_t loading_total_bytes; off_t loading_loaded_bytes; time_t loading_start_time; off_t loading_process_events_interval_bytes; /* Fast pointers to often looked up command */ struct redisCommand *delCommand, *multiCommand, *lpushCommand, *lpopCommand, *rpopCommand; /* Fields used only for stats */ time_t stat_starttime; /* Server start time */ long long stat_numcommands; /* Number of processed commands */ long long stat_numconnections; /* Number of connections received */ long long stat_expiredkeys; /* Number of expired keys */ long long stat_evictedkeys; /* Number of evicted keys (maxmemory) */ long long stat_keyspace_hits; /* Number of successful lookups of keys */ long long stat_keyspace_misses; /* Number of failed lookups of keys */ size_t stat_peak_memory; /* Max used memory record */ long long stat_fork_time; /* Time needed to perform latest fork() */ long long stat_rejected_conn; /* Clients rejected because of maxclients */ long long stat_sync_full; /* Number of full resyncs with slaves. */ long long stat_sync_partial_ok; /* Number of accepted PSYNC requests. */ long long stat_sync_partial_err;/* Number of unaccepted PSYNC requests. */ //保存慢日志命令 list *slowlog; /* SLOWLOG list of commands */ long long slowlog_entry_id; /* SLOWLOG current entry ID */ long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */ unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */ /* The following two are used to track instantaneous "load" in terms * of operations per second. */ long long ops_sec_last_sample_time; /* Timestamp of last sample (in ms) */ long long ops_sec_last_sample_ops; /* numcommands in last sample */ long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; int ops_sec_idx; /* Configuration */ int verbosity; /* Loglevel in redis.conf */ int maxidletime; /* Client timeout in seconds */ int tcpkeepalive; /* Set SO_KEEPALIVE if non-zero. */ int active_expire_enabled; /* Can be disabled for testing purposes. */ size_t client_max_querybuf_len; /* Limit for client query buffer length */ int dbnum; /* Total number of configured DBs */ int daemonize; /* True if running as a daemon */ clientBufferLimitsConfig client_obuf_limits[REDIS_CLIENT_LIMIT_NUM_CLASSES]; /* AOF persistence */ int aof_state; /* REDIS_AOF_(ON|OFF|WAIT_REWRITE) */ int aof_fsync; /* Kind of fsync() policy */ char *aof_filename; /* Name of the AOF file */ int aof_no_fsync_on_rewrite; /* Don't fsync if a rewrite is in prog. */ int aof_rewrite_perc; /* Rewrite AOF if % growth is > M and... */ off_t aof_rewrite_min_size; /* the AOF file is at least N bytes. */ off_t aof_rewrite_base_size; /* AOF size on latest startup or rewrite. */ off_t aof_current_size; /* AOF current size. */ int aof_rewrite_scheduled; /* Rewrite once BGSAVE terminates. */ pid_t aof_child_pid; /* PID if rewriting process */ list *aof_rewrite_buf_blocks; /* Hold changes during an AOF rewrite. */ sds aof_buf; /* AOF buffer, written before entering the event loop */ int aof_fd; /* File descriptor of currently selected AOF file */ int aof_selected_db; /* Currently selected DB in AOF */ time_t aof_flush_postponed_start; /* UNIX time of postponed AOF flush */ time_t aof_last_fsync; /* UNIX time of last fsync() */ time_t aof_rewrite_time_last; /* Time used by last AOF rewrite run. */ time_t aof_rewrite_time_start; /* Current AOF rewrite start time. */ int aof_lastbgrewrite_status; /* REDIS_OK or REDIS_ERR */ unsigned long aof_delayed_fsync; /* delayed AOF fsync() counter */ int aof_rewrite_incremental_fsync;/* fsync incrementally while rewriting? */ int aof_last_write_status; /* REDIS_OK or REDIS_ERR */ int aof_last_write_errno; /* Valid if aof_last_write_status is ERR */ /* RDB persistence */ long long dirty; /* Changes to DB from the last save */ long long dirty_before_bgsave; /* Used to restore dirty on failed BGSAVE */ pid_t rdb_child_pid; /* PID of RDB saving child */ struct saveparam *saveparams; /* Save points array for RDB */ int saveparamslen; /* Number of saving points */ char *rdb_filename; /* Name of RDB file */ int rdb_compression; /* Use compression in RDB? */ int rdb_checksum; /* Use RDB checksum? */ time_t lastsave; /* Unix time of last successful save */ time_t lastbgsave_try; /* Unix time of last attempted bgsave */ time_t rdb_save_time_last; /* Time used by last RDB save run. */ time_t rdb_save_time_start; /* Current RDB save start time. */ int lastbgsave_status; /* REDIS_OK or REDIS_ERR */ int stop_writes_on_bgsave_err; /* Don't allow writes if can't BGSAVE */ /* Propagation of commands in AOF / replication */ redisOpArray also_propagate; /* Additional command to propagate. */ /* Logging */ char *logfile; /* Path of log file */ int syslog_enabled; /* Is syslog enabled? */ char *syslog_ident; /* Syslog ident */ int syslog_facility; /* Syslog facility */ /* Replication (master) */ int slaveseldb; /* Last SELECTed DB in replication output */ long long master_repl_offset; /* Global replication offset */ int repl_ping_slave_period; /* Master pings the slave every N seconds */ char *repl_backlog; /* Replication backlog for partial syncs */ long long repl_backlog_size; /* Backlog circular buffer size */ long long repl_backlog_histlen; /* Backlog actual data length */ long long repl_backlog_idx; /* Backlog circular buffer current offset */ long long repl_backlog_off; /* Replication offset of first byte in the backlog buffer. */ time_t repl_backlog_time_limit; /* Time without slaves after the backlog gets released. */ time_t repl_no_slaves_since; /* We have no slaves since that time. Only valid if server.slaves len is 0. */ int repl_min_slaves_to_write; /* Min number of slaves to write. */ int repl_min_slaves_max_lag; /* Max lag of <count> slaves to write. */ int repl_good_slaves_count; /* Number of slaves with lag <= max_lag. */ /* Replication (slave) */ char *masterauth; /* AUTH with this password with master */ char *masterhost; /* Hostname of master */ int masterport; /* Port of master */ int repl_timeout; /* Timeout after N seconds of master idle */ redisClient *master; /* Client that is master for this slave */ redisClient *cached_master; /* Cached master to be reused for PSYNC. */ int repl_syncio_timeout; /* Timeout for synchronous I/O calls */ int repl_state; /* Replication status if the instance is a slave */ off_t repl_transfer_size; /* Size of RDB to read from master during sync. */ off_t repl_transfer_read; /* Amount of RDB read from master during sync. */ off_t repl_transfer_last_fsync_off; /* Offset when we fsync-ed last time. */ int repl_transfer_s; /* Slave -> Master SYNC socket */ int repl_transfer_fd; /* Slave -> Master SYNC temp file descriptor */ char *repl_transfer_tmpfile; /* Slave-> master SYNC temp file name */ time_t repl_transfer_lastio; /* Unix time of the latest read, for timeout */ int repl_serve_stale_data; /* Serve stale data when link is down? */ int repl_slave_ro; /* Slave is read only? */ time_t repl_down_since; /* Unix time at which link with master went down */ int repl_disable_tcp_nodelay; /* Disable TCP_NODELAY after SYNC? */ int slave_priority; /* Reported in INFO and used by Sentinel. */ char repl_master_runid[REDIS_RUN_ID_SIZE+1]; /* Master run id for PSYNC. */ long long repl_master_initial_offset; /* Master PSYNC offset. */ /* Replication script cache. */ dict *repl_scriptcache_dict; /* SHA1 all slaves are aware of. */ list *repl_scriptcache_fifo; /* First in, first out LRU eviction. */ int repl_scriptcache_size; /* Max number of elements. */ /* Synchronous replication. */ list *clients_waiting_acks; /* Clients waiting in WAIT command. */ int get_ack_from_slaves; /* If true we send REPLCONF GETACK. */ /* Limits */ unsigned int maxclients; /* Max number of simultaneous clients */ unsigned long long maxmemory; /* Max number of memory bytes to use */ int maxmemory_policy; /* Policy for key eviction */ int maxmemory_samples; /* Pricision of random sampling */ /* Blocked clients */ unsigned int bpop_blocked_clients; /* Number of clients blocked by lists */ list *unblocked_clients; /* list of clients to unblock before next loop */ list *ready_keys; /* List of readyList structures for BLPOP & co */ /* Sort parameters - qsort_r() is only available under BSD so we * have to take this state global, in order to pass it to sortCompare() */ int sort_desc; int sort_alpha; int sort_bypattern; int sort_store; /* Zip structure config, see redis.conf for more information */ size_t hash_max_ziplist_entries; size_t hash_max_ziplist_value; size_t list_max_ziplist_entries; size_t list_max_ziplist_value; size_t set_max_intset_entries; size_t zset_max_ziplist_entries; size_t zset_max_ziplist_value; time_t unixtime; /* Unix time sampled every cron cycle. */ long long mstime; /* Like 'unixtime' but with milliseconds resolution. */ /* Pubsub */ dict *pubsub_channels; /* Map channels to list of subscribed clients */ list *pubsub_patterns; /* A list of pubsub_patterns */ int notify_keyspace_events; /* Events to propagate via Pub/Sub. This is an xor of REDIS_NOTIFY... flags. */ /* Cluster */ int cluster_enabled; /* Is cluster enabled? */ mstime_t cluster_node_timeout; /* Cluster node timeout. */ char *cluster_configfile; /* Cluster auto-generated config file name. */ struct clusterState *cluster; /* State of the cluster */ int cluster_migration_barrier; /* Cluster replicas migration barrier. */ /* Scripting */ lua_State *lua; /* The Lua interpreter. We use just one for all clients */ redisClient *lua_client; /* The "fake client" to query Redis from Lua */ redisClient *lua_caller; /* The client running EVAL right now, or NULL */ dict *lua_scripts; /* A dictionary of SHA1 -> Lua scripts */ mstime_t lua_time_limit; /* Script timeout in milliseconds */ mstime_t lua_time_start; /* Start time of script, milliseconds time */ int lua_write_dirty; /* True if a write command was called during the execution of the current script. */ int lua_random_dirty; /* True if a random command was called during the execution of the current script. */ int lua_timedout; /* True if we reached the time limit for script execution. */ int lua_kill; /* Kill the script if true. */ /* Assert & bug reporting */ char *assert_failed; char *assert_file; int assert_line; int bug_report_start; /* True if bug report header was already logged. */ int watchdog_period; /* Software watchdog period in ms. 0 = off */ };
涉及的点:
typedef struct redisDb { //id是本数据库的序号,为0-15(默认Redis有16个数据库) int id; //存储数据库所有的key-value dict *dict; //键的过期时间,字典的键为键,字典的值为过期 UNIX 时间戳 dict *expires; //blpop 存储阻塞key和客户端对象 dict *blocking_keys; //阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象 dict *ready_keys; //存储watch监控的的key和客户端对象 dict *watched_keys; //存储的数据库对象的平均ttl(time to live),用于统计 long long avg_ttl; //List of key names to attempt to defrag one by one, gradually. list *defrag_later; } redisDb;
# 切换数据库 。默认情况下,一个客户端连接到数据库0
select index
# 清除当前数据库下的所有数据
flushdb
# 清楚当前实例下所有的数据库内的数据
flushall
# ping指令测试当前数据库是否联通
ping
# echo指定输出语句
echo [要输出的字符]
# move指令将指定名称的key移动到指定数据库索引下的数据库
move key_name db_Index
# dbsize指令查看当前数据库下的key数量
dbsize
Redis 是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由一 redis.h/redisDb 结构表示,其中,redisDb 结构的dict 字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)
typedef struct redisDB(
//…
…
//数据库的键空间
dict *dict;
//…
)redisDB;
键空间和用户所见的数据库是直接对应的:
键空间的键也就是数据库的键,每个键都是一个字符串对象。
键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种 Redis 对象。
其他键空间操作
除了上面列出的添加、删除、更新、取值操作之外,还有很多针对数据库本身的 Redis 命令,也是通过对键空间进行处理来完成的
比如说:
//通用命令,帮助命令 127.0.0.1:6379> help @generic //删除key 127.0.0.1:6379> del test //删除多个key 127.0.0.1:6379> del test1 test2 (integer) 2 //key是否存在 127.0.0.1:6379> EXISTS test1 (integer) 1 //key的过期秒数 127.0.0.1:6379> expire test1 3 (integer) 1 //移除过期时间 127.0.0.1:6379> persist url (integer) 1 //key重命名 127.0.0.1:6379> rename url url1234 OK //类型检查 127.0.0.1:6379> type url string
键的生存时间或过期时间介绍
redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:
typedef struct redisDB(
//…
…
//数据库的键空间
dict *dict;
//过期字典,保存着键的过期时间
dict *expires;
//…
)redisDB;
过期相关命令
通过 EXPIRE 命令或者 PEXPIRE 命令,客户端市可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为 0 的键;
Redis 有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
EXPIRE命令用于将键 key 的生存时间设置为 ttl 秒。
PEXPIRE命令用于将键 key 的生存时间设置为 ttl 毫秒。
EXPIREAT命令用于将键 key 的过期时间设置为 timestamp 所指定的秒数时间戳。
PEXPIREAT命令用于将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。
虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAT 三个命令都是使用 PEXPIREAT 命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行 PEXPIREAT 命令一样。
127.0.0.1:6379> ttl msg
(integer) -1
在为键设置了生存时间或者过期时间之后,用户可以使用 TTL 命令或者 PTTL 命令查看键的剩余生存时间,即键还有多久才会因为过期而被移除。
如果一个键过期了,那么它什么时候会被删除呢?
这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:
定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
Redis 服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用 CPU 时间和避免浪费内存空间之间取得平衡。
惰性删除实现
因为每个被访问的键都可能因为过期而被 expirelfNeeded 函数删除,所以每个命令的实现函数都必须能同时处理键存在以及键不存在这两种情况:
当键存在时,命令按照键存在的情况执行。
当键不存在或者键因为过期而被 expirelfNeeded 函数删除时,命令按照键不存在的情况执行。
定期删除实现
过期键的定期删除策略由 redisc/activeExpireCycle 函数实现,每当 Redis 的服务器周期性操作 redisc/serverCron 函数执行时, activeExpireCycle 函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。
activeExpireCycle 函数的工作模式可以总结如下:
Redis 服务器是一个事件驱动程序,服务器需要处理以下两类事件:
文件事件(fileevent):Redis 服务器通过套接字与客户端(或者其他 Redis 服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
时间事件(timeevent):Redis 服务器中的一些操作(比如 serverCron 函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(fileeventhandler):
文件事件处理器使用 1/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对
应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 1/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
文件事件处理器的四个组成部分,它们分别是
套接字会有序同步到同一个队列
Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求,比如说:
Redis 的 I/O 多路复用程序的所有功能都是通过包装常见的 select、epoll、evport 和 kqueue 这些!/O 多路复用函数库来实现的,每个 I/O 多路复用函数库在 Redis 源码中都对应一个单独的文件,比如 ae_select.c、ae_epoll.c、ae_kqueue.c,诸如此类。
因为 Redis 为每个 I/O 多路复用函数库都实现了相同的 API,所以 I/O 多路复用程序的底层实现是可以互换的,如图所示。
如果套接字没有任何事件被监听,那么函数返回 AE_NONE。
如果套接字的读事件正在被蓝听,那么函数返回 AE_READABLE。
如果套接字的写事件正在被监听,那么函数返回 AE_WRITABLE。
如果套接字的读事件和写事件正在被监听,那么函数返回 AE_READABLE | AE_WRITABLE.
Redis 在 I/O 多路复用程序的实现源码中用#include 宏定义了相应的规则,程序会在编译时 自动选择系统中性能最高的 I/O 多路复用函数库来作为 Redis 的 I/O 多路复用程序的底层实现
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
# ifdef HAVE_EVPORT
# include "ae_evport.c"
# else
# ifdef HAVE_EPOLL
# include "ae_epoll.c"
# else
# ifdef HAVE_KQUEUE
# include "ae_kqueue.c"
# else
# include "ae_select.c"
# endif
# endif
# endif
引用知乎上一个高赞的回答来解释什么是 I/O 多路复用。假设你是一个老师,让 30 个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
第一种就是阻塞 IO 模型,第三种就是 I/O 复用模型。
Linux 系统有三种方式实现 IO 多路复用:select、poll 和 epoll。
例如 epoll 方式是将用户 socket 对应的 fd 注册进 epoll,然后 epoll 帮你监听哪些 socket 上有消息到达,这样就避免了大量的无用操作。此时的 socket 应该采用非阻塞模式。
这样,整个过程只在进行 select、poll、epoll 这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的 reactor 模式。
Redis 的时间事件分为以下两类:
定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序 X 在当前时间的 30 毫秒之后执行一次。
周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序 Y 每隔 30 毫秒就执行一次。
一个时间事件主要由以下三个属性组成:
id:服务器为时间事件创建的全局唯一 ID(标识号)。ID 号按从小到大的顺序递增,新事件的 ID 号比旧事件的 ID 号要大。
when:毫秒精度的 UNIX 时间戳,记录了时间事件的到达(arrive)时间。
timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。
一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:
如果事件处理器返回 ae.h/AE_NOMORE,那么这个事件为定时事件:该事件在达到一次之后就会被删除,之后不再到达。
如果事件处理器返回一个非 AE_NOMORE 的整数值,那么这个事件为周期性时间:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的 when 属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。比如说,如果一个时间事件的处理器返回整数值 30,那么服务器应该对这个时间事件进行更新,让这个事件在 30 毫秒之后再次到达。
用链表连接起来的三个时间事件
注意,我们说保存时间事件的链表为无序链表,指的不是链表不按 ID 排序,而是说,该链表不按 when 属性的大小排序。正因为链表没有按 when 属性进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理。
时间事件应用实例:serverCron 函数
持续运行的 Redis 服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由 redis.c/serverCron 函数负责执行,它的主要工作包括:
更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
清理数据库中的过期键值对。
关闭和清理连接失效的客户端。
尝试进行 AOF 或 RDB 持久化操作。
如果服务器是主服务器,那么对从服务器进行定期同步。
如果处于集群模式,对集群进行定期同步和连接测试。
Redis 服务器以周期性事件的方式来运行 serverCron 函数,在服务器运行期间,每隔一段时间,serverCron 就会执行一次,直到服务器关闭为止。
在 Redis2.6 版本,服务器默认规定 serverCron 每秒运行 10 次,平均每间隔 100 毫秒运行一次。
从 Redis28 开始,用户可以通过修改 hz 选项来调整 serverCron 的每秒执行次数,具体信息请参考示例配置文件 redisconf 关于 hz 选项的说明。
以下是事件的调度和执行规则:
daemonize 介绍
daemonize 设置yes
或者no
区别
daemonize:yes
:redis 采用的是单进程多线程的模式。当 redis.conf 中选项 daemonize 设置成 yes 时,代表开启守护进程模式。在该模式下,redis 会在后台运行,并将进程 pid 号写入至 redis.conf 选项 pidfile 设置的文件中,此时 redis 将一直运行,除非手动 kill 该进程。daemonize:no
: 当 daemonize 选项设置成 no 时,当前界面将进入 redis 的命令行界面,exit 强制退出或者关闭连接工具(putty,xshell 等)都会导致 redis 进程退出。Redis 服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类。
文件事件处理器是基于 Reactor 模式实现的网络通信程序。
文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生。
文件事件分为 AEREADABLE 事件(读事件)和 AEWRITABLE 事件(写事件)两类。
时间事件分为定时事件和周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次。
服务器在一般情况下只执行 serverCron 函数一个时间事件,并且这个事件是周期性事件。
文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。
时间事件的实际处理时间通常会比设定的到达时间晚一些。
RDB 持久化功能所生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态.
有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。
SAVE 命令执行时服务器的状态
RDB 文件保存和载入
因为 BGSAVE 命令的保存工作是由子进程执行的,所以在子进程创建 RDB 文件的过程中,Redis 服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同。
因为 BGREWRITEAOF 和 BGSAVE 两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑–并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,
那么只要满足以下三个条件中的任意一个,BGSAVE 命令就会被执行:
服务器在 900 秒之内,对数据库进行了至少 1 次修改。
服务器在 300 秒之内,对数据库进行了至少 10 次修改。
服务器在 60 秒之内,对数据库进行了至少 10000 次修改。
对应的配置文件信息在 saveparams 数组中
save 900 1 //900秒内如果超过1个key被修改,则发起快照保存
save 300 10 //300秒内容如超过10个key被修改,则发起快照保存
save 60 10000
struct redisServer {
// ...
struct saveparam *saveparams;//记录了save保存条件的数组
// ...
};
struct saveparam {
time_t seconds;//秒数
int changes;//修改数
};
dirty 计数器和 lastsave 属性
除了 saveparams 数组之外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性:
struct redisServer {
// ...
long long dirty;//修改计数器
time_t lastsave;//上一次执行保存的时间
// ...
};
Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所设置的保存条件是否已经满足,如果满足的话,就执行 BGSAVE 命令。
def serverCron():
# ...
# 遍历所有保存条件
for saveparam in server.saveparams:
# 计算距离上次执行保存操作有多少秒
save_interval = unixtime_now()-server.lastsave
# 如果数据库状态的修改次数超过条件所设置的次数
# 并且距离上次保存的时间超过条件所设置的时间
# 那么执行保存操作
if server.dirty >= saveparam.changes and \
save_interval > saveparam.seconds:
BGSAVE()
# ...
RDB 文件为二进制格式保存,下面我们为了演示效果,采用字符串的形式演示
一个 RDB 文件的 databases 部分可以保存任意多个非空数据库。例如,如果服务器的 0 号数据库和 3 号数据库非空,那么服务器将创建一个如下图所示的 RDB 文件
每个非空数据库在 RDB 文件中都可以保存为 SELECTDB、db_number、key_value_pairs 三个部分:
下图则展示了一个完整的 RDB 文件,文件中包含了 0 号数据库和 3 号数据库
key_value_pairs 都保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内
不带过期时间的键值对结构
不带过期时间的键值对在 RDB 文件中由 TYPE、key、value3 部分组成:
下图展示了一个没有过期时间的字符串键值对:
带有过期时间的键值对结构
带有过期时间的键值对在 RDB 文件中由 EXPIRETIME_MS、ms、TYPE、key、value 五部分组成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fi08OX2W-1672365188515)(null)]
假设 0 号数据库中有 2 个不带有过期时间的键值对,数据库 3 含有 1 个不带有过期时间的键值对和 1 个带有过期时间的键值对
RDB 文件中的每个 value 部分都保存了一个值对象,每个值对象的类型都由与之对应的 TYPE 记录,根据类型的不同,value 部分的结构、长度也会有所不同
如果 TYPE 的值为 REDIS_RDB_TYPE_STRING,那么 value 保存的就是一个字符串对象,字符串对象的编码可以是 REDIS_ENCODING_INT 或者 REDIS_ENCODING_RAW
① 如果字符串对象的编码为 REDIS_ENCODING_INT:
那么说明对象中保存的是长度不超过 32 位的整数,这种编码的对象将以下图所示的结构保存
其中,ENCODING 的值可以是 REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16 或者 REDIS_RDB_ENC_INT32 三个常量的其中一个,它们分别代表 RDB 文件使用 8 位(bit)、16 位或者 32 位来保存整数值 integer。举个例子,如果字符串对象中保存的是可以用 8 位来保存的整数 123,那么这个对象在 RDB 文件中保存的结构将如下图所示
② 如果字符串对象的编码为 REDIS_ENCODING_RAW:
那么说明对象所保存的是一个字符串值,根据字符串长度的不同,有压缩和不压缩两种方法来保存这个字符串:
备注:以上两个条件是在假设服务器打开了 RDB 文件压缩功能的情况下进行的,如果服务器关闭了 RDB 文件压缩功能,那么 RDB 程序总以无压缩的方式保存字符串值,对于没有被压缩的字符串,RDB 程序会以下图所示的结构来保存该字符串
对于压缩后的字符串,RDB 程序会以下图所示的结构来保存该字符串
其中,REDIS_RDB_ENC_LZF 常量标志着字符串已经被 LZF 算法(http://liblzf.plan9.de)压缩过了,读入程序在碰到这个常量时,会根据之后的compressed_len、origin_len和compressed_string三部分,对字符串进行解压缩:其中compressed_len记录的是字符串被压缩之后的长度,而origin_len记录的是字符串原来的长度,compressed_string记录的则是被压缩之后的字符串
如果 TYPE 的值为 REDIS_RDB_TYPE_LIST,那么 value 保存的就是一个 REDIS_ENCODING_LINKEDLIST 编码的列表对象
RDB 文件保存这种对象的结构如下图所示:
示例
如果 TYPE 的值为 REDIS_RDB_TYPE_SET,那么 value 保存的就是一个 REDIS_ENCODING_HT 编码的集合对象
RDB 文件保存这种对象的结构如下图所示:
示例,下图展示了一个包含四个元素的集合,结构中的第一个数字 4 记录了集合的大小,之后跟着的是集合的四个元素:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGzKG7UR-1672365181687)(http://qinyingjie.top/blogImg/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=-20220912235141288.png)]
如果 TYPE 的值为 REDIS_RDB_TYPE_HASH,那么 value 保存的就是一个 REDIS_ENCODING_HT 编码的集合对象
RDB 文件保存这种对象的结构如下图所示:
结构中的每个键值对都以键紧挨着值的方式排列在一起,如下图所示:
因此,从更详细的角度看,哈希表对象的结构可以表示为下图所示:
作为示例,下图展示了一个包含两个键值对的哈希表,第一个数字 2 记录了哈希表的键值对数量,之后跟着的是两个键值对:
如果 TYPE 的值为 REDIS_RDB_TYPE_ZSET,那么 value 保存的就是一个 REDIS_ENCODING_SKIPLIST 编码的有序集合对象
RDB 文件保存这种对象的结构如下图所示:
sorted_set_size 记录了有序集合的大小,也即是这个有序集合保存了多少元素,读入程序需要根据这个值来决定应该读入多少有序集合元素
以 element 开头的部分代表有序集合中的元素,每个元素又分为成员(member)和分值(score)两部分,成员是一个字符串对象,分值则是一个 double 类型的浮点数,程序在保存 RDB 文件时会先将分值转换成字符串对象,然后再用保存字符串对象的方法将分值保存起来
有序集合中的每个元素都以成员紧挨着分值的方式排列,下图所示:
因此,从更详细的角度看,有序集合对象的结构可以表示为下图所示:
作为示例,下图展示了一个带有两个元素的有序集合,第一个数字 2 记录了有序集合的元素数量,之后跟着的是两个有序集合元素:
在执行 SAVE 命令或者 BGSAVE 命令创建一个新新的 RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。
将对 RDB 文件进行载入:
除了 RDB 持久化功能之外,Redis 还提供了 AOF(Append Only File)持久化功能。与 RDB 持久化通过保存数据库中的键值对来记录数据库状态不同,AOF 持久化是通过保存 Re dis 服务器所执行的写命令来记录数据库状态的
AOF 持久化功能的 实现可以分为命令追加 (append)、文件写入、 文件同步(sync) 三个步骤。
命令追加
当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾;
文件写入
Redis 的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到 aof buf 缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,考虑是否需要将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面.
文件同步
flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定,各个不同同值产生的行为如表:
appendfsync no: 当设置 appendfsync 为 no 时,Redis 不会主动调用 fsync 去将 aof 日志同步到磁盘,完全依赖于操作系统
appendfsync everysec: 当设置为 appendfsync 为 everysec 的时候,Redis 会默认每隔一秒进行一次 fsync 调用,将缓冲区中的数据写到磁盘。默认是这种方式.
appendfsync always: 当设置 appendfsync 为 always 时,每一次写操作都会调用一次 fsync,这时数据是最安全的,当然,由于每次都会执行 fsync,所以其性能也会受到影响。
系统提供了 fsync 和 fdatasync 两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。
appendonly yes #启用aof持久化方式
# appendfsync always #每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
# appendfsync no #完全依赖os,性能最好,持久化没保证
因为 AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态
Redis 读取 AOF 文件并还原数据库状态的详细步骤如下:
因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的。所以随着服务器运行时间的流逝,AOF 文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的 AOF 文件很可能对 Redis 服务器、甚至整个宿主计算机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 文件重写(rewrite)功能。通过该功能,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新日两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常会比旧 AOF 文件的体积要小得多。节省空间.
虽然 Redis 将生成新 AOF 文件替换旧 AOF 文件的功能命名为 AOF 文件重写”,但实际上,AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。copy on write
所有类型的键都可以用同样的方法去减少 AOF 文件中的命令数量。 首先从数据库中读取键现在的值,然后用一条命令去记录键值对, 代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
因为 aof_rewrite 函数生成的新 AOF 文件只包含还原当前数据库状态所必须的命令,所以新 AOF 文件不会浪费任何硬盘空间。
在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了 redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。
在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值为 64,这也就是说,如果一个集合键包含了超过 64 个元素,那么重写程序会用多条 SADD 命令来记录这个集合,并且每条命令设置的元素数量也为 64 个;
为了解决服务器进程和子进程这种数据不一致问题,Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区.
这也就是说,在子进程执行 AOF 重写期间,服务器进程需要执行以下三个工作:
1)执行客户端发来的命令。
2)将执行后的写命令追加到 AOF 缓冲区。
3)将执行后的写命令追加到 AOF 重写缓冲区。
AOF 工作原理
AOF 重写和 rdb 创建快照一样,都是巧妙的利用了写时复制机制:
redis 调用 fork ,现在有父子两个进程子进程
根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
父进程继续处理 client 请求,除了把写命令写入到原来的 aof 文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
现在父进程可以使用临时文件替换老的 aof 文件,并重命名,后面收到的写命令也开始往新的 aof 文件中追加。
BGREWRITEAOF 实现原理
aof_rewrite 函数的缺点:上面介绍的 AOF 重写程序 aof_rewrite 函数可以很好地完成创建一个新 AOF 文件的任务, 但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因 为 Redis 服务器使用单个线程来处理命令请求,所以如果由服务器直接调用 aof_rewrite 函数的 话,那么在重写 AOF 文件期间,服务期将无法处理客户端发来的命令请求
很明显,作为一种辅佐性的维护手段,Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到子进程里执行,这样做可以同时达到两个目的:
当服务器以 AOF 持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么 AOF 文件不会因头这个过期键而产生任何影响。
当过期键被惰性册除或者定期删除之后,程序会向 AOF 文件追加(append)一条 DEL 命令,来显式地记录该键已被删除。
redis 使用了 2 种方式进行持久化:
RDB 和 AOF 兼容性:
默认情况下,Redis 将数据库快照保存在一个 dump.rdb 的二进制文件中。
如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态。
只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。
写时复制(copy-on-write/COW)技术:
写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
这是一种简单的读写分离思想,适用于读多写少的并发场景。比如黑白名单,热点文章等等。正常情况下我们说 cow,指的是修改共享资源时,将共享资源 copy 一份,加锁后修改,再将原容器的引用指向新的容器。对于 java 来说,是有线程的 cow 容器的,比如 CopyOnWriteArrayList。另外就是 cow 保证的是最终一致性而不是强一致。
Redis 的 cow
struct redisServer {
// ...
list *clients;// 一个链表,保存了所有客户端状态
// ...
};
Redis 服务器状态结构的 clients 属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历 clients 链表来完成;
typedef struct client { uint64_t id; // 客户端唯一ID,通过全局变量server.next_client_id实现。 int fd; // socket的文件描述符。 redisDb *db; // select命令选择的数据库对象 robj *name; // 客户端名称,可以使用命令CLIENT SETNAME设置。 time_t lastinteraction // 客户端上次与服务器交互的时间,以此实现客户端的超时处理。 sds querybuf; //输入缓冲区,recv函数接收的客户端命令请求会暂时缓存在此缓冲区。 int argc; robj **argv; struct redisCommand *cmd; list *reply; unsigned long long reply_bytes; size_t sentlen; char buf[PROTO_REPLY_CHUNK_BYTES]; int bufpos; } client;
属性说明
id 为客户端唯一 ID,通过全局变量 server.next_client_id 实现。
fd 为客户端 socket 的文件描述符。
db 为客户端使用 select 命令选择的数据库对象。
name:客户端名称,可以使用命令 CLIENT SETNAME 设置。
lastinteraction:客户端上次与服务器交互的时间,以此实现客户端的超时处理。
querybuf:输入缓冲区,recv 函数接收的客户端命令请求会暂时缓存在此缓冲区。
argc:输入缓冲区的命令请求是按照 Redis 协议格式编码字符串,需要解析出命令请求的所有参数,参数个数存储在 argc 字段,参数内容被解析为 robj 对象,存储在 argv 数组。
cmd:待执行的客户端命令;解析命令请求后,会根据命令名称查找该命令对应的命令对象,存储在客户端 cmd 字段,可以看到其类型为 struct redisCommand。
reply:输出链表,存储待返回给客户端的命令回复数据。链表节点存储的值类型为 clientReplyBlock
typedef struct clientReplyBlock {
size_t size, used;
char buf[];
} clientReplyBlock;
可以看到链表节点本质上就是一个缓冲区(buffffer),其中 size 表示缓冲区空间总大小,used 表示缓冲区已使用空间大小。
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的 redis.h/redisClient 结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包括:
客户端的套接字描述符。
客户端的名字。
客户端的标志值(flag)。
指向客户端正在使用的数据库的指针,以及该数据库的号码。
客户端当前要执行的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
客户端的输入缓冲区和输出缓冲区。
客户端的复制状态信息,以及进行复制所需的数据结构。
客户端执行 BRPOP、BLPOP 等列表阻塞命令时使用的数据结构。
客户端的事务状态,以及执行 WATCH 命令时用到的数据结构。
客户端执行发布与订阅功能时用到的数据结构。
客户端的身份验证标志。
客户端的创建时间,客户端和服务器最后一次通信的时间,以及客户端的输出缓冲区大小超出软性限制(soft limit)的时间。
客户端状态的 fd 属性记录了客户端正在使用的套接字描述符:
根据客户端类型的不同,fd 属性的值可以是-1 或者是大于-1 的整数:
127.0.0.1:6379> client list
id=9 addr=127.0.0.1:56659 laddr=127.0.0.1:6379 fd=8 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 qbuf=26 qbuf-free=16864 argv-mem=10 multi-mem=0 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=18682 events=r cmd=client|list user=default redir=-1 resp=2
名字(name 属性),在默认情况下,一个连接到服务器的客户端是没有名字的,name 属性为空。可以手动设置.使用 CLIENT setname 命令可以为客户端设置一个名字。
flags 标志:客户端的标志属性 flags 记录了客户端的角色(role),以及客户端目前所处的状态;
每个标志使用一个常量表示,一部分标志记录了客户端的角色:
在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。
在主从服务器进行命令传播期间,从服务器需要向主服务器发送 REPLICATION_ACK 命令,在发送这个命令之前,从服务器必须打开主服务器对应的客户端的 REDIS_MASTER_FORCE_REPLY 标志,否则发送操作会被拒绝执行。
两种特殊客户端
querybuf 属性,对应输入缓冲区
客户端状态的输入缓中区用于保存客户端发送送的命令请求;可以动态的扩大和缩小,最大不能超过 1G,否则会关闭这个客户端.
在服务器将客户端发送的命令请求保存到客户端状态的 querybuf 属性之后,服务器将对命令请求的内容进行分机斤,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv 属性和 argc 属性:
argv 属性是一个数组,数组中的每个项都是一个字符串对象,其中 argv[0]是要执行的命令,而之后的其他项则是传给命令的参数。
argc 属性则负责记录 argv 数组的长度。
命令表中使用字典保存着 redis 的命令字典.且不区分大小写.
当服务器从协议内容中分析并得出 argv 属性和 argc 属性的值之后,服务器将根据项 argv[0]的值,在命令表中查找命令所对应的命令实现函数
命令表格式,下图展示了一个命令表示例,该表是一个字典,字典的键是一个 SDS 结构,保存了命令的名字,字典的值是命令所对应的 redisCommand 结构,这个结构保存了命令的实现函数、 命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的:
客户端的固定大小缓冲区由 buf 和 bufpos 两个属性组成:
通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区 16KB 大小的限制。
服务器使用两种模式来限制客户端输出缓冲区的大小:
客户端状态的 authenticated 属性用于记录客户 端是否通过了身份验证:
当客户端 authenticated 属性的值为 0 时,除除了 AUTH 命令之外,客户端发送的所有其他命令都会被服务器拒绝执行;
typedef struct redisClient {
// ...
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
// ...
} redisClient;
ctime 属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,CLIENT Iist 命令的 age 域记录了这个秒数;
lastinteraction 属性
obuf_soft_limit_reachedtime 属性记录了输出缓冲区大小第一次到达软性限制(soft limit)的时间.
一个普通客户端可以因为多种原因而被关闭:
服务器状态结构使用 clients 链表连接起多个客户端状态,新添加的客户端状态会被放到链表的末尾。
客户端状态的 flags 属性使用不同标志来表示客户端的角色,以及客户端当前所处的状态。
输入缓冲区记录了客户端发送的命令请求,这个缓冲区的大小不能超过 1GB。
命令的参数和参数个数会被记录在客户端状态的 argv 和 argc 属性里面,而 cmd 属性则记录了客户端要执行命令的实现函数。
客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用,其中固定大小缓冲区的最大大小为 16KB,而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制,那么客户端会被立即关闭;除此之外,如果客户端
在一定时间内,一直超过服务器设置的软性限制,那么客户端也会被关闭。
当一个客户端通过网络连接连上服务器时,服务器会为这个客户端创建相应的客户端状态。网络连接关闭、发送了不合协议格式的命令请求、成为 CLIENT KILL 命令的目标、空转时间超时、输出缓冲区的大小超出限制,以上这些原因都会造成客户端被关闭。
处理 Lua 脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。
·载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。
客户端向服务器发送命 令请求 SET KEY VALUE。
服务器接收并处理客户端发来的命令请求 SET KEY VALUE,在数据库中进行设置操作,并产生命令回复 OK。
服务器将命令回复 OK 发送给客户端。
客户端接收服务器返回的命令回复 OK,并将这个回复打印给用户观看。
客户端从用户得到命令请求后转换为协议格式,再连接服务器的套接字,再将协议格式的命令发送给服务器
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
查找命令
第一件事根据 argv[0] 参数在命令表中查找命令,并保存到客户端状态的 cmd 属性中命令表是一个字典,键为字符串对象,值为 redisCommand 结构记录 redis 命令实现信息 redisCommand 结构主要属性:
struct redisCommand {
char *name;//命令名
redisCommandProc *proc;//命令执行函数
int arity; //参数个数,-N代表参数个数>=N,正数表示参数个数为N
char *sflags; //命令的sflags属性字符串
int flags; //从sflags获取的整数mask值
//获取key参数的可选函数,当下面3种情况都无法确定key参数的时候才需要使用该函数
redisGetKeysProc *getkeys_proc;
int firstkey; //第一个key的位置,0表示没有key
int lastkey; //最后一个key的位置;负数计算为正数第(argc+lastkey)个
int keystep; //参数为 key,val,key,val,...格式,第一个和最后一个key之间的key跨步
long long microseconds;//命令从服务启动到现在的执行时间,单位:微秒
long long calls;//命令从服务启动到现在的执行的次数
};
属性说明
name:命令名称。
proc:命令处理函数。
arity:命令参数数目,用于校验命令请求格式是否正确;当 arity 小于 0 时,表示命令参数数目大于等于 arity;当 arity 大于 0 时,表示命令参数数目必须为 arity;注意命令请求中,命令的名称本身也是一个参数,如 get 命令的参数数目为 2,命令请求格式为 get key。
sflags:命令标志,例如标识命令时读命令还是写命令,详情参见表 9-2;注意到 sflags 的类型为字符串,此处只是为了良好的可读性。
flags:命令的二进制标志,服务器启动时解析 sflags 字段生成。
calls:从服务器启动至今命令执行的次数,用于统计。
microseconds:从服务器启动至今命令总的执行时间,
microseconds/calls 即可计算出该命令的平均处理时间,用于统计。
属性中的 sflags 属性可以使用的标识符有:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2da7KdKf-1672365181690)(http://qinyingjie.top/blogImg/webp)]
执行预操作
执行命令之前还需要一些预操作
检查客户端状态的 cmd 是否指向 NULL,是则找不到命令实现,返回错误
根据 cmd 指向的 redisCommand 结构的 arity 属性,检查命令的个数是否匹配,否则不执行返回错误
检查客户端是否通过了身份验证,没有则只能执行 AUTH 命令,否则则返回错误
如果服务器打开了 maxmemory 功能,会先检查服务器的内存占用情况,并在有需要时进行内存回收,如果回收失败则不再执行后续步骤,返回错误
如果服务器上一次执行 BGSAVE 命令出差,且打开了 stop-writes-on-bgsave-error 功能
如果客户端在使用 SUBSCRIBE 命令订阅频道,或者在使用 PSUBSCRIBE 模式,则只会执行 SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE 四个命令,其他则拒绝
如果服务器在进行数据载入,客户端发送的命令需要带有 l 标识(INFO、SHUTDOWN、PUBLISH)才会被服务器执行
如果服务器执行 Lua 脚本超时而阻塞,则只会执行 SHUTDOWN nosave 和 SCRIPT KILL 命令
如果客户端在执行事务,服务器只会执行客户端的 EXEC、DISCARD、MULTI、WATCH 命令,其他会被放进事务队列
如果服务器打开了监视器功能,服务器则先把执行的命令和参数发给监视器,然后才真正执行命令
调用命令实现函数
由于 cmd 已经保存了命令实现,命令参数、个数已经保存在 argv、argc 中,只需要执行
client -> cmd -> proc(client)
命令实现函数执行指定操作,并参数相应回复,保存在客户端输出缓冲区 (buf、reply 属性),再为客户端的套接字关联命令回复处理器
执行后续工作
如果服务器开启慢查询,慢查询模块需要检查是否为执行完的命令添加一条新的慢查询日志
根据耗费时长,更新 redisCommand 结构的 milliseconds 属性,并将 calls 计数器 + 1
如果 AOF 开启了,则会将命令写入 AOF 缓冲区
如果有其他服务器正在复制当前的服务器,也会将命令传播给所有从服务器
将命令发送给客户端
命令实现函数会将输出回复保存在输出缓冲区里面,并为客户端套接字关联命令回复处理器,当客户端套接字可写时,服务器则会执行命令回复处理器,将保存在客户端输出缓冲区的命令回复发送给客户端
客户端接受并打印命令回复
当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看
默认每隔 100 毫秒运行一次,负责管理服务器资源,并保持服务器自身的良好运作
更新服务器时间缓存
获取系统当前时间需要执行系统调用,为了减少次数,服务器中的 unixtmie 和 mstime 属性被用作当前时间的缓存,因为 serverCron 函数默认会以每 100 毫秒一次的频率更新 unixtime 属性和 mstime 属性,所以这两个属性记录的时间的精确度并不高
服务器只会在打印日志、更新服务器的 LRU 时钟、决定是否执行持久化任务、计算服务器上线时间等对时间精确度要求不高的功能上使用上述两个属性
对于键过期时间、添加慢查询日志这种高精度时间的功能仍旧执行系统调用
struct redisServer {
// ...
//保存了秒级精度的系统当前UNIX 时间戳
time_t unixtime;
//保存了毫秒级精度的系统当前UNIX 时间戳
long long mstime;
// ...
};
更新 LRU 时钟
struct redisServer {
// ...
//默认每10 秒更新一次的时钟缓存,
//用于计算键的空转(idle )时长。
unsigned lruclock:22;
// ...
};
lruclock 保存了服务器的 LRU 时钟,为时间缓存的一种,默认每 10 秒更新一次
每个对象都有一个 lru 属性,保存了对象最后一次被命令访问的时间
数据库键的空转时间(减法):
lruclock - lru
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
更新服务器每秒执行命令次数
struct redisServer { // ... //上一次进行抽样的时间 long long ops_sec_last_sample_time; //上一次抽样时,服务器已执行命令的数量 long long ops_sec_last_sample_ops; // REDIS_OPS_SEC_SAMPLES 大小(默认值为16 )的环形数组, //数组中的每个项都记录了一次抽样结果。 long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]; // ops_sec_samples 数组的索引值, //每次抽样后将值自增一, //在值等于16 时重置为0 , //让ops_sec_samples 数组构成一个环形数组。 int ops_sec_idx; // ... };
serverCron 中的 trackOpearationPerSecond 函数会以每 100 毫秒频率执行,功能是以抽样方式方式估算记录服务器在最近一秒钟处理的命令请求数量,通过 Info stats 命令的 instanceous_ops_sec 域查看
127.0.0.1:6379> INFO stats
#Stats
total_connections_received:2
total_commands_processed:3
instantaneous_ops_per_sec:0
trackOpearationPerSecond 函数和服务器状态中四个 opssec 开头的属性有关
trackOpearationPerSecond 函数每次运行会根据 ops_sec_last_sample_time 记录的上一次抽样时间和服务器当前时间,以及 ops_sec_last_sample_ops 记录的上一次抽样的已执行命令数量和服务器当前的已执行数量,计算两次调用之间服务器平均每毫秒处理了多少请求,再计算一秒钟服务器能处理多少请求的估计值,再作为新数组项放进 ops_sec_samples 环形数组里
执行 INFO 命令时服务器调用 getOperationPerSecond 函数,根据 ops_sec_samples 环形数组中抽样结果,计算 instanceous_ops_per_sec 属性的值
更新服务器内存峰值记录
struct redisServer {
// ...
//已使用内存峰值
size_t stat_peak_memory;
// ...
};
服务器状态的 stat_peak_memory 记录了服务器内存峰值大小
serverCron 函数执行时服务器则查看当前使用内存数量,与 stat_peak_memory 进行大小对比
127.0.0.1:6379> info memory
# Memory
used_memory_peek:692456
used_memory_peek_human:676.23K
used_memory_peek 和 used_memory_peek_human 是两种不同格式记录
处理 SIGTERM 信号
服务器启动后服务器进程的 SIGTERM 信号会关联 sigtermHandler 函数,负责在服务器接到 SIGTERM 信号时,打开服务器状态的 shutdown_asap 标识
// SIGTERM 信号的处理器
static void sigtermHandler(int sig) {
//打印日志
redisLogFromHandler(REDIS_WARNING,"Received SIGTERM, scheduling shutdown...");
//打开关闭标识
server.shutdown_asap = 1;
}
serverCron 函数则是对服务器状态的 shutdown_asap 属性检查,根据只决定是否关闭服务器
服务器在关闭自身之前会进行 RDB 持久化操作,对 SIGTERM 信号拦截
管理客户端资源
serverCron 函数会调用 clientsCron 函数对一定数量的客户端进行检查:
连接超时,即很长时间没有互动,释放客户端
输入缓冲区超过一定长度,释放客户端当前输入缓冲区,重新创建一个默认大小的输入缓冲区,防止耗费内存
管理数据库资源
serverCron 函数会调用 databasesCron 函数对一部分数据库检查,删除过期键,并在有需要时对字典进行收缩操作
执行被延迟的 BGREWRITEAOF
在执行 BGSAVE 期间,如果客户端向服务器发来 BGREWRITEAOF 命令,则会延迟到 BGSAVE 命令结束之后
服务器的 aof_rewrite_scheduled 标识了是否延迟,1 为延迟
serverCron 函数会检查 BGSAVE 或 BGREWRITEAOF 是否在运行,如果没有且 aof_rewrite_scheduled 为 1,则执行被延迟的 BGREWRITEAOF 命令
检查持久化操作的运行状态
服务器状态使用 rdb_child_pid 属性和 aof_child_pid 属性记录了执行 BGSAVE 和 BGREWRITEAOF 命令子进程的 ID,可以用于检查 BGSAVE 和 BGREWRITEAOF 是否正在执行,如果为 -1 表示没有在执行
serverCron 函数执行时会检查这两个属性,如果有一个不为 -1,则程序会执行 wait3 函数,检查子进程是否有信号发来服务器进程:
有信号到达,则新的 RDB 文件生成,或者 AOF 文件重写完成,服务器需要进行新的操作,如新的文件代替旧的文件
没有则持久化未完成,不做动作
如果都为 -1,则服务器没有在持久化,则
将 AOF 缓冲区中的内容写入 AOF 文件
如果服务器开启了 AOF 持久化功能,并且 AOF 缓冲区里面还有待写入的数据,那么 serverCron 函数会调用相应的程序,将 AOF 缓冲区中的内容写入到 AOF 文件里面
关闭异步客户端
关闭输出缓冲区大小超过限制的客户端
增加 cronloops 计数值
服务器状态的 cronloops 记录了 serverCron 函数执行次数,作用为在复制模块实现每执行 N 次操作执行一次指定代码
struct redisServer {
// ...
// serverCron 函数的运行次数计数器
// serverCron 函数每执行一次,这个属性的值就增一。
int cronloops;
// ...
};
int main(int argc, char **argv) {
...
initServerConfig();
...
loadServerConfig(configfile,options);
...
initServer();
...
loadDataFromDisk();
...
InitServerLast();
...
aeMain(server.el);
}
redis 服务初始化分为六个阶段:
初始化服务器状态结构
第一步是创建一个 struct redisServer 类型的实例变量 server 作为服务器状态,并为结构中的各个属性设置默认值
初始化 server 由 redis.c/initServerConfig 完成,主要工作:
设置运行 ID
设置默认运行频率
设置默认配置文件路径
设置运行架构
设置默认端口号
设置默认 RDB 持久化条件和 AOF 持久化条件
初始化服务器的 LRU 时钟
创建命令表
void initServerConfig(void){ //设置服务器的运行id getRandomHexChars(server.runid,REDIS_RUN_ID_SIZE); //为运行id 加上结尾字符 server.runid[REDIS_RUN_ID_SIZE] = '\0'; //设置默认配置文件路径 server.configfile = NULL; //设置默认服务器频率 server.hz = REDIS_DEFAULT_HZ; //设置服务器的运行架构 server.arch_bits = (sizeof(long) == 8) ? 64 : 32; //设置默认服务器端口号 server.port = REDIS_SERVERPORT; // ... }
载入配置选项
启动服务器时可以给定配置参数或者指定配置文件来修改服务器的默认配置
在 initServerConfig 函数初始化完成后,就会载入用户给定的参数对 server 变量的属性进行修改
服务器在用 initServerConfig 函数初始化完 server 变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对 server 变量相关属性的值进行修改
初始化服务器数据结构(initServer 函数)
initServerConfig 函数只创建了命令表,但还有部分数据结构需要创建
server.clients 链表:客户端状态链表,redisClient 结构
server.db 数组:服务器所有数据库
server.pubsub_channels 字典:保存频道订阅信息
server.subpub_patterns 链表:保存模式订阅信息
server.lua:保存 Lua 脚本的环境
server.slowlog:保存慢查询日志
调用 initServer 函数为以上数据结构分配数据,并在有需要时设置默认值或关联初始化值,服务器到这一步才初始化数据结构是因为服务器需要载入用户指定的配置选项才能对数据结构正确初始化,如果在 initServerConfig 就初始化,而用户配置不同的值,导致重新调整和修改
initServer 函数还进行了:
初始完成则在日志中打印 Redis 图标及相关版本信息
还原数据库状态
执行事件循环
在初始化的最后一步,服务器将打印出以下日志,并开始执行服务器的事件循环(loop)
一个命令请求从发送到完成主要包括以下步骤:1)客户端将命令请求发送给服务器;2)服务器读取命令请求,并分析出命令参数;3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;4)服务器将命令回复返回给客户端。
serverCron 函数默认每隔 100 毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的 SIGTERM 信号,管理客户端资源和数据库状态,检查并执行持久化操作等等。
服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
执行 SLAVOF 命令或设置 slaveof 选项,让一个服务器去复制另外一个服务器,被复制的为主服务器,对主服务器进行复制的是从服务器
进行复制的主从服务器双方的数据库将保存相同的数据,即数据库状态一致,简称“一致”
复制功能分为同步和命令传播两个操作
同步操作(sync):作用于服务器的数据库状态更新至主服务器当前所处的数据库状态
命令传播(command propagate):则作用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态
旧版复制功能(SYNC 命令)
客户端向从服务器发送 SLAVEOF 命令复制主服务器时,首先从服务器执行的是同步操作,更新至主服务器当前所处的数据库状态
实际操作是从服务器向主服务器发送 SYNC 命令:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MEyQmwOG-1672365181690)(http://qinyingjie.top/blogImg/%E5%90%8C%E6%AD%A5%E4%BE%8B%E5%AD%90.45dj1qzhcii0.png)]
同步操作之后,主从服务器两者的数据库状态将达到一致状态,但当主服务器执行客户端的命令时,主服务器的状态可能修改,不再一致
为了让主从服务器再一次回到一致状态,主服务器需要对从服务器执行命令传播操作:
将造成主从服务器不一致的写命令发送给从服务器执行,执行后再一次回到一致状态
从服务器对主服务器的复制可以分为以下两种情况:
初次复制:从服务器没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同
断线后重复制:处于命令传播阶段的主从服务器因网络原因中断了复制,但从服务器通风自动重连接重新连上了主服务器,并继续复制主服务器
初次复制使用旧版复制功能你那个很好地完成任务,但是对于断线后重复制来说,旧版复制功能效率低
在命令传播阶段某个时间点断线后主服务器执行了部分命令修改了数据库状态,而从服务器还在重连,成功后是执行 SYNC 命令重新开始操作,但是实际上不是很需要的,因为主从服务器在断线之前是一致状态的
SYNC 命令
使用 PSYNC 命令代替 SYNC 命令解决断线重复制的低效问题,
PSYNC 命令有完整重同步和部分重同步两种模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1obdjlQ-1672365181691)(http://qinyingjie.top/blogImg/PSYNC%E8%BF%87%E7%A8%8B.b55p88goz8o.png)]
部分重同步功能由以下三个部分构成:
复制偏移量
主服务器和从服务器会分别维护一个复制偏移量:
通过对比偏移量可以知道主从服务器是否处于一致状态
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认为 1 MB,模型是滑动窗口.
当主服务器进行命令传播时,不仅会将写命令发给所有从服务器,还会将写命令入队到复制积压缓冲区里面,且队列中每个字节记录相应的复制偏移量
当从服务器重新连上主服务器后,从服务器会通过 PSYNC 命令将自己发复制偏移量 offset 发送给主服务器,主服务器根据此来决定对服务器执行何种同步操作:
根据需要调整复制缓冲区大小
默认为 1 MB,最小跟由公式 second * write_size_per_second 估算
second:服务器断线后重新连上主服务器的平均时间,单位秒
write*size_per_second:服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和)
安全起见,实际大小可以设置为:2 * second _ write_size_per_second,可以保证绝大部分断线情况都能用部分重同步处理
服务器运行 ID
当从服务器进行初次复制时,主服务器会将自己的运行 ID 传送给从服务器,而从服务器会将其保存,当从服务器断线并重新连上一个主服务器时,从服务器将当前连接的主服务器发送之前保存的运行 ID:
PSYNC 命令的调用方法有两种:
如果从服务器没有复制过任何主服务器,或者之前执行了 SLAVEOF no one 命令,则在开始一次新的复制时向主服务器发送 PSYNC ? -1 命令,主动请求主服务器进行完整重同步
否则发送 PSYNC 命令
服务器会返回以下三种回复之一:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0ey1RNC-1672365181691)(http://qinyingjie.top/blogImg/PSYNC%E5%91%BD%E4%BB%A4%E5%AE%8C%E6%95%B4%E8%BF%87%E7%A8%8B.170urt0l2uw0.png)]
SLAVEOF <master_ip> <master_port>
步骤 1:设置主服务器的地址和端口
从服务器将客户端输入的主服务器的 IP 地址和端口保存到服务器状态的 masterhost 和 masterport 属性中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OkqzRrJC-1672365181691)(http://qinyingjie.top/blogImg/%E4%BB%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%8A%B6%E6%80%81.3j1v68mqeq20.png)]
SLAVEOF 是一个异步命令,完成以上两个属性的设置之后,从服务器向客户端返回 OK,表示复制指令已经被接收,但实际的复制工作将在 OK 之后才真正执行
struct redisServer {
// ...
//主服务器的地址
char *masterhost;
//主服务器的端口
int masterport;
// ...
};
步骤 2:建立套接字连接
根据 IP 地址和端口,从服务器创建连向主服务器的套接字连接,如果连接成功,从服务器将为此套接字关联一个专门用于处理复制工作的文件事件处理器,负责后续的工作
主服务器在接收从服务器的套接字之后为该套接字创建相应的客户端状态,并将从服务器看作一个连接到主服务器的客户端看待,此时从服务器将具有服务器和客户端两种状态:从服务器可以向主服务器发送命令请求,而主服务器则会向从服务器返回命令回复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dSlij4oJ-1672365181691)(http://qinyingjie.top/blogImg/%E6%AD%A5%E9%AA%A42.8dpjsasewso.png)]
步骤 3:发送 PING 命令
从服务器成为客户端后第一件事是向主服务器发送一个 PING 命令
虽然主从服务器成功建立起了套接字连接,但双方并未使用该套接字进行过任何通信, 通过发送 PING 命令可以检查套接字的读写状态是否正常
因为复制工作接下来的几个步骤都必须在主服务器可以正常处理命令请求的状态下才能进行,通过发送 PING 命令可以检查主服务器能否正常处理命令请求
从服务器将遇到三种情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzpQhTuz-1672365181691)(http://qinyingjie.top/blogImg/%E6%AD%A5%E9%AA%A43.7upf6jfq2pc.png)]
步骤 4:身份验证
下一步则是决定是否身份验证:根据是否设置了 masterauth 选项
从服务器向主服务器发送了一条 AUTH 命令,参数为从服务器 masterauth 选项的值
可能遇到几种情况:
错误情况令从服务器中止目前的复制工作,并从创建套接字重新开始,直到身份验证或者从服务器放弃执行复制
步骤 5:发送端口信息
从服务器执行 REPLCONF listening-port ,向主服务器发送从服务器的监听端口号
主服务器接收后记录在从服务器对应的客户端状态的 slave_listening_port 中,其唯一作用就是主服务器执行 INFO replication 时打印从服务器端口
typedef struct redisClient {
// ...
//从服务器的监听端口号
int slave_listening_port;
// ...
} redisClient;
步骤 6:同步
从服务器向主服务器发送 PSYNC 命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前所处的状态,执行之后主服务器也会成为从服务器的客户端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6PfTVu4-1672365181691)(http://qinyingjie.top/blogImg/%E4%BA%92%E7%9B%B8%E6%88%90%E4%B8%BA%E5%AE%A2%E6%88%B7%E7%AB%AF.5nc4ilscoq40.png)]
正因为主服务器成为了从服务器的客户端,所以主服务器才可以通过发送写命令来改变从服务器的数据库状态,不仅同步操作需要用到这一点,这也是主服务器对从服务器执行命令传播操作的基础
步骤 7:命令传播
当完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了
命令传播阶段,从服务器默认以每秒一次频率向服务器发送命令:
REPLCONF ACK <replication_offset>
replication_offset:从服务器当前的复制偏移量
三个作用:
检测主从服务器的网络连接状态
通过接受命令来检查网络连接是否正常,如果没有收到则连接出现问题
辅助实现 min-slaves 配置选项
min-slaves-to-write 和 min-slaves-max-lag 可以防止主服务器在不安全的情况下执行写命令
min-slaves-to-write3
min-slaves-max-lag10
那么在从服务器的数量少于 3 个,或者三个从服务器的延迟(lag)值都大于或等于 10 秒时,主服务器将拒绝执行写命令,这里的延迟值就是上面提到的 INFO replication 命令的 lag 值。
检测命令丢失
根据两个 REPLCONF ACK 命令可以得出从服务器的写命令是否在半路丢失,从复制偏移量的比较得出
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。
3.2 版本后 从库判断已过期会返回空值
因为 master 无法及时提供 DEL 命令,为了解决这个问题,slave 使用了逻辑时钟来报告 key 不存在
通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在干主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。
哨兵功能:哨兵的核心功能是主节点的自动故障转移。
Sentinel 哨兵组成了一个 Redis 高可用性的方案:
由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器及其所有从服务器,并在主服务器下线时将其从服务器升级为新的主服务器,继续执行要求,当先前的主服务器再次上线时则成为新的从服务器
redis-sentinel /path/to/yourt/setinel.conf
# or
redis-server /path/to/yourt/setinel.conf -- sentinel
启动时执行以下:
初始化 Sentinel 服务器与普通服务器的区别:
Sentinel 本质是一个运行在特殊模式下的 Redis 服务器,所以启动第一步也是初始化一个 Redis 服务器.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUFllGfh-1672365181692)(http://qinyingjie.top/blogImg/Sentinel%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84Redis%E6%9C%8D%E5%8A%A1%E5%99%A8.2lgtr1qzz4c0.png)]
例如使用不同端口和不同的命令列表
#define REDIS_SERVERPORT 6379
#define REDIS_SENTINEL_PORT 26379
除此之外,普通 Redis 服务器使用 redis.c/redisCommandTable 作为服务器的命令表,而 Sentinel 则使用 sentinel.c/sentinelcmds 作为服务器的命令表:
struct redisCommand redisCommandTable[] = { {"get",getCommand,2,"r",0,NULL,1,1,1,0,0}, {"set",setCommand,-3,"wm",0,noPreloadGetKeys,1,1,1,0,0}, {"setnx",setnxCommand,3,"wm",0,noPreloadGetKeys,1,1,1,0,0}, // ... {"script",scriptCommand,-2,"ras",0,NULL,0,0,0,0,0}, {"time",timeCommand,1,"rR",0,NULL,0,0,0,0,0}, {"bitop",bitopCommand,-4,"wm",0,NULL,2,-1,1,0,0}, {"bitcount",bitcountCommand,-2,"r",0,NULL,1,1,1,0,0} } struct redisCommand sentinelcmds[] = { {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0}, {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0}, {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0}, {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0}, {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0} };
服务器会初始化一个 sentinel.c/sentinelState 结构,保存了服务器中 Sentinel 功能有关的状态,一般状态是
struct sentinelState { //当前纪元,用于实现故障转移 uint64_t current_epoch; //保存了所有被这个sentinel 监视的主服务器 //字典的键是主服务器的名字 //字典的值则是一个指向sentinelRedisInstance 结构的指针 dict *masters; //是否进入了TILT 模式? int tilt; //目前正在执行的脚本的数量 int running_scripts; //进入TILT 模式的时间 mstime_t tilt_start_time; //最后一次执行时间处理器的时间 mstime_t previous_time; // 一个FIFO 队列,包含了所有需要执行的用户脚本 list *scripts_queue; } sentinel;
struct sentinelRedisInstance
typedef struct sentinelRedisInstance { //标识值,记录了实例的类型,以及该实例的当前状态 int flags; //实例的名字 //主服务器的名字由用户在配置文件中设置 //从服务器以及Sentinel 的名字由Sentinel 自动设置 //格式为ip:port ,例如"127.0.0.1:26379" char *name; //实例的运行ID char *runid; //配置纪元,用于实现故障转移 uint64_t config_epoch; //实例的地址 sentinelAddr *addr; // SENTINEL down-after-milliseconds 选项设定的值 //实例无响应多少毫秒之后才会被判断为主观下线(subjectively down ) mstime_t down_after_period; // SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的quorum 参数 //判断这个实例为客观下线(objectively down )所需的支持投票数量 int quorum; // SENTINEL parallel-syncs <master-name> <number> 选项的值 //在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量 int parallel_syncs; // SENTINEL failover-timeout <master-name> <ms> 选项的值 //刷新故障迁移状态的最大时限 mstime_t failover_timeout; // ... } sentinelRedisInstance;
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
因为 Sentinel 需要与多个实例创建多个网络连接,所以 Sentinel 使用的是异步连接
下图展示了一个 Sentinel 向被它监视的两个主服务器 master1 和 master2 创建命令连接和订阅连接的例子:
Sentinel 会默认每 10 秒一次频率向监视的主服务器通过命令连接发送 INFO 命令,通过分析回复获取当前主服务器的信息
主服务器本身的信息:
主服务器从属的所有从服务器信息
根据 run_id 和 role 域,Sentinel 将对主服务器的实例结构进行更新,例如重启后的主服务器 run_id 将不同,Sentinel 检测后将会对实例结构的 run_id 进行更新
主服务器返回的从服务器信息被用于更新主服务器实例的 slave 字典
主服务器实例结构的 flags 属性的值是 SRI_MASTER,从服务器实例结构的 flags 属性的值是 SRT_SLAVE
主服务器的 name 属性是 Sentinel 配置文件设置的,从服务器实例结构的 name 属性的值是 Sentinel 根据从服务器的 IP 地址、端口号设置的
如果监视的主服务器有新的从服务器出现时,Sentinel 会为这个新的从服务器创建相应的实例结构外,还会创建连接到从服务器的命令连接和订阅连接
创建命令连接后,Sentinel 会以每十秒一次的频率通过命令连接向从服务器发送 INFO 命令
根据 INFO 命令的回复,Sentinel 会提取以下信息:
默认情况下:Sentinel 会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello "<s_ip>,<s_port>, <s_runid>,<s_epoch>,<m_name>, <m_ip>,<m_port>,<m_epoch>"
这条命令向服务器的sentinel: hello 频道发送了一条信息,信息的内容由多个参数组成:
1.其中以 s_开头的参数记录的是 Sentinel 本身的信息,见下图
2.而 m_开头的参数记录的则是主服务器的信息,各个参数的意义见下图
信息中和 Senti inel 有关的参数
参数 | 意义 |
---|---|
s_ip | Sentinel 的 IP 地址 |
s_port | Sentinel 的端端口号 |
s_runid | Sentinel 的运运行 ID |
s_epoch | Sentinel 当前的配置纪元 |
和主服务相关
参数 | 意义 |
---|---|
m_name | 主服务器的的名字 |
m_ip | 主服务器的 IP 地址 |
m_port | 主服务器的的端口号 |
m_epoch | 主服务器当前的配置纪元 |
对于监视同一个服务器的多个 Sentinel 来说,一个 Sentinel 发送的信息会被其他 Sentinel 接收到,这些信息会被用于更新其他 Sentinel 对发送信息 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视服务器的认知。
建立订阅连接后,Sentinel 则会向服务器发送以下命令:
SUBCRIBE _sentinel_:hello
此订阅会持续到与服务器的连接断开,Sentinel 通过此连接接收和发送消息
对于监视同一个服务器的多个 Sentinel,一个 Sentinel 发送的消息也会被其他 Sentinel 接收到,然后被用于更新对发送消息的 Sentinel 的认知,也会更新其监控的服务器的认知
更新 sentinels 字典
Sentinel 的 sentinels 字典 同样监视这个主服务器的其他 Sentinel:
键:其他 Sentinel 的名字,格式为 ip:port
值:对应的 Sentinel 的实例结构
当一个 Sentinel 接收到其他 Sentinel 发来的信息时,目标 Sentinel 会提取出以下参数:
与 Sentinel 有关的参数:源 Sentinel 的 IP、端口、run_id、配置纪元
与主服务器有关的参数:源 Sentinel 正在监视的主服务器的名字、IP、端口、配置纪元
根据主服务器参数,目标 Sentinel 会在自己的 Sentinel 状态的 masters 字典查找对应的主服务器实例的结构,根据参数,检查主服务器实例结构的 Sentinels 字典中 Sentinel 实例结构是否存在:
创建连向其他 Sentinel 的命令连接
当 Sentinel 通过频道信息发现新的 Sentinel 后不仅创建对应的实例结构,也会创建连向此的命令连接,最终监视同一主服务器的多个 Sentinel 将形成相互连接的网络
Sentinel 之间不会创建订阅连接
Sentinel 在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他 Sentinel 时,却只会创建命令连接,而不创建订阅连接。这是因为 Sentinel 需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新 Sentinel,所以才需要建立订阅连接,而相互已知的 Sentinel 只要使用命令连接来进行通信就足够了
默认情况下,Sentinel 会默认每秒一次的频率向所有与它创建了命令连接的实例(主服务器、从服务器、其他 Sentinel ) 发送 PING 命令,通过回复判断实例是否在线
实例对 PING 命令的回复有两种:
Sentinel 配置文件中的 down-after-millisecond 的值为 50000 毫秒,当主服务器 master 连续 50000 毫秒内都向 Sentinel 返回无效回复,则会被标记为主观下线,对应的实例结构的 flags 被标识为 SRI_S_DOWN
down-after-millisecond 的值不仅会被 Sentinel 用来判断主服务器的主观下线状态,还会被用于判断主服务器属下的从服务器,以及所有同样监视这个主服务器的其他 Sentinel 的主观下线状态
且监视同一个主服务器的不同 Sentinel 来说,此值可能不同
需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
当一个主服务器被认为主观下线后,Sentinel 会询问其他监视此主服务器的 Sentinel,当接收到足够多的下线判断(主观或者客观下线)后,才会将主服务器判断为客观下线,并对主服务器执行故障转移操作
发送 SENTINEL is-master-down-by-addr 命令
Sentinel 使用
SENTINEL IS-master-down-by-addr <current_epoch> <run_id>
来询问其他 Sentinel 是否同意主服务器已下线
接收 SENTIENL is-master-down-by-addr 命令
目标 Sentinel 接收到源 Sentinel 发来的命令后,目标会分析提取参数,根据主服务器 IP 和端口检查主服务器是否已下线,然后向源返回一个包含三个参数的 Multi Bulk 作为回复
接收 SENTIENL is-master-down-by-addr 命令的回复
根据其他 Sentinel 发回的命令回复,统计同意已下线的数量,达到配置的数量则判断为客观下线,flags 标志为 SRI_O_DOWN
Sentinel 配置的 quorum 参数判断的标志,大于等于则被认为是客观下线
不同的 Sentinel 此参数的配置不同,即对主服务器的客观下线判断不同
领头 Sentinel 对下线主服务器进行故障转移操作,需要先选举 领头 Sentinel
选举领头 Sentinel :
三个步骤:
在已下线的主服务器的从服务器中选出一个从服务器,成为新的主服务器
让其他从服务器改为复制新的主服务器
将已下线的主服务器设置为新的主服务器的从服务器,当重新上线时自动成为主服务器的从服务器
选出新的主服务器
挑选一个状态良好、数据完整的从服务器,发送 SLAVEOF no one 成为主服务器
领头 Sentinel 将所有属于的从服务器保存在列表中,然后按照以下规则过滤
修改从服务器的复制目标
让其他从服务器复制新的主服务器,领头 Sentinel 发送 SLAVEOF 命令
将旧主服务器变为从服务器
领头 Sentinel 发送命令
(1)哨兵节点的数量应不止一个,一方面增加哨兵节点的冗余,避免哨兵本身成为高可用的瓶颈;另一方面减少对下线的误判。此外,这些不同的哨兵节点应部署在不同的物理机上。
(2)哨兵节点的数量应该是奇数,便于哨兵通过投票做出“决策”:领导者选举的决策、客观下线的决策等。
(3)各个哨兵节点的配置应一致,包括硬件、参数等;此外,所有节点都应该使用 ntp 或类似服务,保证时间准确、一致。
(4)哨兵的配置提供者和通知客户端功能,需要客户端的支持才能实现,如前文所说的 Jedis;如果开发者使用的库未提供相应支持,则可能需要开发者自己实现。
(5)当哨兵系统中的节点在 docker(或其他可能进行端口映射的软件)中部署时,应特别注意端口映射可能会导致哨兵系统无法正常工作,因为哨兵的工作基于与其他节点的通信,而 docker 的端口映射可能导致哨兵无法连接到其他节点。例如,哨兵之间互相发现,依赖于它们对外宣称的 IP 和 port,如果某个哨兵 A 部署在做了端口映射的 docker 中,那么其他哨兵使用 A 宣称的 port 无法连接到 A。
Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
一个 Redis 集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以使用 CLUSTER MEET 命令来完成,该命令的格式如下:
CLUSTER MEET <ip> <port>
向一个节点 node 发送 CLUSTER MEET 命令,可以让 node 节点与 ip 和 port 所指定的节点进行握手(hand shake),当握手成功时,node 节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。
节点加入到集群
启动节点
一个节点就是一个运行在集群模式下的 Redis 服务器,Redis 服务器在启动时会根据 cluster-enabled 配置选项是否为 yes 来决定是否开启服务器的集群模式
redis 节点的功能作用
节点会继续使用文件事件处理器来处理命令设请求和返回命令回复。
节点会继续使用时间事件处理器来执行 s erverCron 函数,而 serverCron 函数又会调用集群模式特有的 clusterCron 函数。clusterCron 函数负责执行在集群模式下需要执行的常规操作,例如,向集群中的其他节点发送 Gossip 消息,检查节点是否断线,或者检查是否需要对下线节点进行自动故障转移等。
节点会继续使用数据库来保存键值对数据,键值对依然会是各种不同类型的对象。
节点会继续使用 RDB 持久化模块和 AOF 持久化模块来执行持久化工
作。
节点会继续使用发布与订阅模块来执行 PUBLISH、SUBSCRIBE 等
命令。
节点会继续使用复制模块来进行节点的复制工作。
节点会继续使用 Lua 脚本环境来执行客户端输入的 Lua 脚本。
clusterNode 结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的 IP 地址和端口号等等。
每个节点都会使用一个 clusterNode 结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode 结构,以此来记录其他节点的状态;
typedef struct clusterNode { mstime_t ctime; /* Node object creation time. */ char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */ int flags; /* REDIS_NODE_... */ uint64_t configEpoch; /* Last configEpoch observed for this node */ unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */ int numslots; /* Number of slots handled by this node */ int numslaves; /* Number of slave nodes, if this is a master */ struct clusterNode **slaves; /* pointers to slave nodes */ struct clusterNode *slaveof; /* pointer to the master node */ mstime_t ping_sent; /* Unix time we sent latest ping */ mstime_t pong_received; /* Unix time we received the pong */ mstime_t fail_time; /* Unix time when FAIL flag was set */ mstime_t voted_time; /* Last time we voted for a slave of this master */ mstime_t repl_offset_time; /* Unix time we received offset for this node */ PORT_LONGLONG repl_offset; /* Last known repl offset for this node. */ char ip[REDIS_IP_STR_LEN]; /* Latest known IP address of this node */ int port; /* Latest known port of this node */ clusterLink *link; /* TCP/IP link with this node */ list *fail_reports; /* List of nodes signaling this as failing */ } clusterNode;
clusterNode 结构的 link 属性是一个 clusterL ink 结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区.
clusterNode 结构的 link 属性是一个 clusterLink 结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区
typedef struct clusterLink { //连接的创建时间 mstime_t ctime; // TCP 套接字描述符 int fd; //输出缓冲区,保存着等待发送给其他节点的消息(message )。 sds sndbuf; //输入缓冲区,保存着从其他节点接收到的消息。 sds rcvbuf; //与这个连接相关联的节点,如果没有的话就为NULL struct clusterNode *node; } clusterLink;
redisClient 结构和 clusterLink 结构的相同和不同
redisClient 结构和 clusterLink 结构都有自己的套接字描述符和输入、输出缓冲区,这两个结构的区别在于,redisClient 结构中的套接字和缓冲区是用于连接客户端的,而 clusterlink 结构中的套接字和缓冲区则是用于连接节点的。
每个节点都保存着一个 clusterState 结构, 这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元,诸如此类:
typedef struct clusterState { //指向当前节点的指针 clusterNode *myself; //集群当前的配置纪元,用于实现故障转移 uint64_t currentEpoch; //集群当前的状态:是在线还是下线 int state; //集群中至少处理着一个槽的节点的数量 int size; //集群节点名单(包括myself 节点) //字典的键为节点的名字,字典的值为节点对应的clusterNode 结构 dict *nodes; // ... } clusterState;
通过向节点 A 发送 CLUSTER MEET 命令,客户端可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里面:
CLUSTER MEET <ip> <port>
收到命令的节点 A 将与节点 B 进行握手(hand shake),以此来确认彼此的存在,并为将来的进一步通信打好基础:
之后,节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点,让其他节点也与节点 B 进行握手,最终,经过一段时间之后,节点 B 会被集群中的所有节点认识。
Redis 集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽.
当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)
Redis 集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。
通过向节点发送 CLUSTER ADDSLOTS 命令,我们可以将一个或多个槽指派(assign) 给节点负责:
127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4... 5000
OK
127.0.0.1:7000> CLUSTER NODES
记录指派信息
struct clusterNode {
// ...
unsigned char slots[16384/8];
int numslots;
// ...
};
clusterNode 结构的 slots 属性和 numslot 属性记录了节点负责处理哪些槽:
slots 属性是一个二进制位数组(bit array),这个数组的长度为 16384/8=2048 个字节,共包含 16384 个二进制位。
Redis 以 0 为起始索引,16383 为终止索引,对 slots 数组中的 16384 个二进制位进行编号,并根据索引 i 上的二进制位的值来判断节点是否负责处理槽 i:
图 17-9 展示了一个 slots 数组示例:这个数组索引 0 至索引 7 上的二进制位的值都为 1,其余所有二进制位的值都为 0,这表示节点负责处理槽 0 至槽 7。
因为取出和设置 slots 数组中的任意一个二进制位的值的复杂度仅为 O(1),所以对于一个给定节点的 slots 数组来说,程序检查节点是否负责处理某个槽,又或者将某个槽指派给节点负责,这两个动作的复杂度都是 O (1)
至于 numslots 属性则记录节点负责处理的槽的数量,也即是 slots 数组中值为 1 的二进制位的数量。
传播节点的槽指派信息
一个节点除了会将自己负责处理的槽记录在 clusterNode 结构的 slots 属性和 numslots 属性之外,它还会将自己的 slots 数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
当节点 A 通过消息从节点 B 那里接收到节点 B 的 slots 数组时,节点 A 会在自己的 clusterState.nodes 字典中查找节点 B 对应的 clusterNode 结构,并对结构中的 slots 数组进行保存或者更新。
因为集群中的每个节点都会将自己的 slots 数组通过消息发送给集群中的其他节点,并且每个接收到 slots 数组的节点都会将数组保存到相应节点的 clusterNode 结构里面,因此,集群中的每个节点都会知道数据库中的 16384 个槽分别被指派给了集群中的哪些节点。
记录集群所有槽的指派信息
typedef struct clusterState {
// ...
clusterNode *slots[16384];
// ...
} clusterState;
clusterState 结构中的 slots 数组记录了集群中所有 16384 个槽的指派信息;
slots 数组包含 16384 个项,每个数组项都是一个指向 clusterNode 结构的指针:
如果只将槽指派信息保存在各个节点的 clusterNode slots 数组里,会出现一些无法高效地解决的问题,而 clusterState.slots 数组 的存在解决了这些问题:
如果节点只使用 clusterNode.slots 数组来记录槽的指派信息,那么为了知道槽 i 是否已经被指派,或者槽 i 被指派给了哪个节点,程序需要遍历 clusterState.nodes 字典中的所有 clusterNode 结构,检查这些结构的 slots 数组,直到找到负责处理槽 i 的节点为止,这个过程的复杂度为 O (N),其中 N 为 clusterState.nodes 字典保存的 clusterNode 结构的数量。而通过将所有槽的指派信息保存在 clusterState.slots 数组里面,程序要检查槽 i 是否已经被指派,又或者取得负责处理槽 i 的节点,只需要访问 clusterState. slots[i]的值即可,这个操作的复杂度仅为 O(1)
clusterState 和 clusterNode 的 slots 的区别
要说明的一点是,虽然 clusterState.slots 数组记录了集群中所有槽的指派信息,但使用 clusterNode 结构的 slots 数组来记录单个节点的槽指派信息仍然是有必要的:
因为当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,程序只需要将相应节点的 clusterNode.slots 数组整个发送出去就可以了。
另一方面,如果 Redis 不使用 clusterNode.slots 数组,而单独使用 clusterState.slots 数组的话,那么每次要将节点 A 的槽指派信息传播给其他节点时,程序必须先遍历整个 clusterState.slots 数组,记录节点 A 负责处理哪些槽,然后才能发送节点 A 的槽指派信息,这比直接发送 clusterNode.slots 数组要麻烦和低效得多。clusterState.slots 数组记录了集群中所有槽的指派信息,而 clusterNode.slots 数组只记录了 clusterNode 结构所代表的节点的槽指派信息,这是两个 slots 数组的关键区别所在。
CLUSTER ADDSLOTS 命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责;
对 1,2 执行命令后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wnqKWgC-1672365181695)(http://qinyingjie.top/blogImg/image-20220918225912032.png)]
在对数据库中的 16384 个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
节点使用以下算法来计算给定键 key 属于哪个槽:
def slot_ number(key):
return CRC16(key) & 16383
其中 CRC16 (key) 语句用于计算键 key 的 CRC-16 校验和,而&16383 语句则用于计算出一个介于 0 至 16383 之间的整数作为键 key 的槽号。
使用命令可以查看一个给定键属于哪个槽:
CLUSTER KEYSLOT<key>
判断槽节点是否自己负责
当节点计算出键所属的槽 i 之后,节点就会检查自己在 clusterState.slots 数组中的项 i,判断键所在的槽是否由自己负责:
moved 错误
一致性哈希算法为的就是解决分布式缓存的问题
在一致性哈希算法中,整个哈希空间是一个虚拟圆环,
typedef struct clusterState {
/***/
zskiplist *slots_to_keys;
/***/
} clusterState;
slots_to_keys 跳跃表每个节点的分值(score) 都是一个槽号,而每个节点的成员(member) 都是一个数据库键:
Redis 集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点) ,并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线(online) 进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
重新分片原理
Redis 集群的重新分片操作是由 Redis 的集群管理软件 redis-trib 负责执行的,Redis 提供了进行重新分片所需的所有命令,而 redis-trib 则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib 対集群的単个槽 slot 迸行重新分片的歩驟如下:
ASKING 命令功能:唯一要做的就是打开发送该命令的客户端的 REDIS_ASKING 标识
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
ASK 错误和 MOVED 错误都会导致客户端转向,它们的区别在于:
Redis 集群中的节点分为主节点(master) 和从节点(slave) ,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
设置从节点
CLUSTER REPLICATE <node_ id>
可以让接收命令的节点成为 node_id 所指定节点的从节点,并开始对主节点进行复制:
接收到该命令的节点首先会在自己的 clusterState.nodes 字典中找到 node_ id 所对 应节点的 clusterNode 结构,并将自己的 clusterState.myself. slaveof 指针指向这个结构,以此来记录这个节点正在复制的主节点:
然后节点会修改自己在 clusterState.myself.flags 中的属性,关闭原本的 REDIS_NODE_MASTER 标识,打开 REDIS__NODE_SLAVE 标识,表示这个节点已经由原来的主节点变成了从节点。
最后,节点会调用复制代码,并根据 clusterState.myself.slaveof 指向的 clusterNode 结构所保存的 IP 地址和端口号,对主节点进行复制。因为节点的复制功能和单机 Redis 服务器的复制功能使用了相同的代码,所以让从节点复制主节点相当于向从节点发送命令 SLAVEOF。
struct clusterNode {
// ...
//正在复制这个主节点的从节点数量
int numslaves;
// 一个数组
//每个数组项指向一个正在复制这个主节点的从节点的clusterNode 结构
struct clusterNode **slaves;
// ...
};
故障检查
集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,以此来检测对方是否在线,如果接收 PING 消息的节点没有在规定的时间内,向发送 PING 消息的节点返回 PONG 消息,那么发送 PING 消息的节点就会将接收 PING 消息的节点标记为疑似下线(probable fail, PFAIL )
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
新的主节点是通过选举产生的,Redis 中是通过 currentEpoch 和 configEpoch 来解决这些信息更新问题,epoch 的概念与 raft 协议中的 epoch 类似,表示逻辑时钟,每次集群状态更新之后,epoch 都会+1,谁的 epoch 大,谁的 gossip 消息就可信。
currentEpoch 表示当前集群的 epoch,configEpoch 表示当前节点看到的其他节点的 epoch。
以下是集群选举新的主节点的方法:
这个选举新主节点的方法和前面文章介绍的选举领头 Sentinel 的方法非常相似,因为两者都 是基于 Raft 算法的领头选举(leader election)方法来实现的
集群中的各个节点通过发送和接收消息(message) 来进行通信,我们称发送消息的节点为发送者(sender),
接收消息的节点为接收者(receiver)
节点中的消息分为 5 种,消息有消息头和正文组成
一条消息由**消息头(header)和消息正文(data)**组成
消息头(struct clusterMsg)
typedef struct { //消息的长度(包括这个消息头的长度和消息正文的长度) uint32_t totlen; //消息的类型 uint16_t type; //消息正文包含的节点信息数量 //只在发送MEET 、PING 、PONG 这三种Gossip 协议消息时使用 uint16_t count; //发送者所处的配置纪元 uint64_t currentEpoch; //如果发送者是一个主节点,那么这里记录的是发送者的配置纪元 //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元 uint64_t configEpoch; //发送者的名字(ID ) char sender[REDIS_CLUSTER_NAMELEN]; //发送者目前的槽指派信息 unsigned char myslots[REDIS_CLUSTER_SLOTS/8]; //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字 //如果发送者是一个主节点,那么这里记录的是REDIS_NODE_NULL_NAME //(一个40 字节长,值全为0 的字节数组) char slaveof[REDIS_CLUSTER_NAMELEN]; //发送者的端口号 uint16_t port; //发送者的标识值 uint16_t flags; //发送者所处集群的状态 unsigned char state; //消息的正文(或者说,内容) union clusterMsgData data; } clusterMsg;
消息正文
union clusterMsgData { // MEET 、PING 、PONG 消息的正文 struct { //每条MEET 、PING 、PONG 消息都包含两个 // clusterMsgDataGossip 结构 clusterMsgDataGossip gossip[1]; } ping; // FAIL 消息的正文 struct { clusterMsgDataFail about; } fail; // PUBLISH 消息的正文 struct { clusterMsgDataPublish msg; } publish; //其他消息的正文... };
gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有的节点都收到消息,但是能够保证最终所有节点都会收到消息,因此它是一个最终一致性协议。
gossip 协议使用 PING、PONG 类型的消息
通过 geo 相关的 redis 命令计算坐标之间的距离
键空间通知:“某个键执行了什么命令”的通知称为键空间通知(key-space notification)
键事件通知:键事件通知(key-event notification)关注的是“某个命令被什么键执行了”
notify-keyspace-events 选项
Redis 的发布与订阅功能由 PUBLISH、SUBSCRIBE、PSUBSCRIBE 等命令组成。通过执行 SUBSCRIBE 命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message) 时,频道的所有订阅者都会收到这条消息。
客户端执行命令,订阅频道
SUBSCRIBE "news.it"
客户端执行命令,向频道发消息
PUBLISH "news.it" "hello'
发布到频道
订阅频道实现
Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字 典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端:
struct redisServer {
// ...
//保存所有频道的订阅关系
dict *pubsub_channels;
// ...
};
typedef struct pubsubPattern {
//订阅模式的客户端
redisClient *client;
//被订阅的模式
robj *pattern;
} pubsubPattern;
模式的订阅实现
前面说过,服务器将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 属性里面,与此类似,服务器也将所有模式的订阅关系都保存在服务器状态的 pubsub_channels 属 性里面:
PUBSUB CHANNELS
PUBSUB CHANNELS [pattern]子命令用于返回服务器当前被订阅的频道,其中 pattern 参 数是可选的:
PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 channel-2…channel-n]子命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量。这个子命令是通过在 pubsub_channels 字典 中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现的(订 阅者链表的长度就是频道订阅者的数量),
PUBSUB NUMPAT
PUBSUB NUMPAT 子命令用于返回服务器当前被订阅模式的数量。这个子命令是通过返回 pubsub_patterns 链表的长度来实现的,因为这个链表的长度就是服务器被订阅模式的数量
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
事务的特性
一个事务执行的过程,该事务首先以一个 MULT|命令为开始,接着将多个命令放入事务当中,最后由 EXEC 命令将这个事务提交(commit) 给服务器执行
事务的错误处理
事务实现
事务的开始
MULTI 命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的
命令入队
每个 Redis 客户端都有自己的事务状态,这个事务状态保存在客户端状态的 mstate 属性里面:
typedef struct redisClient {
// ...
//事务状态
multiState mstate; /* MULTI/EXEC state */
// ...
} redisClient;
事务状态包含一个事务队列,以及一个已入队命令的计数器(也可以说是事务队列的长度)
typedef struct multiState {
//事务队列,FIFO 顺序
multiCmd *commands;
//已入队命令计数
int count;
} multiState;
事务队列是一个 multiCmd 类型的数组,数组中的每个 multiCmd 结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量
typedef struct multiCmd {
//参数
robj **argv;
//参数数量
int argc;
//命令指针
struct redisCommand *cmd;
} multiCmd;
事务队列以先进先出(FIFO)的方式保存入队的命令,较先入队的命令会被放到数组的 前面,而较后入队的命令则会被放到数组的后面
事务执行
当一个处于事务状态的客户端向服务器发送 EXEC 命令时,这个 EXEC 命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命 令所得的结果全部返回给客户端
如果客户端发送的命令为 EXEC、DISCARD、 WATCH、MULTI|四个命令的其中一个,那么服务器立即执行这个命令。
如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复。
Redis 中的 Multi 和 Pipleline 都可以一次性执行多个命令,
Pipeline 只是把多个 redis 指令一起发出去,redis 并没有保证这些指令执行的顺序,且减少了多次网络传递的开销,因而其执行效率很高;
Multi 相当于一个 redis 的 transaction,保证整个操作的有序性,通过 watch 这些 key,可以避免这些 key 在事务的执行过程中被其它的命令修改,从而导致得的到结果不是所期望的。
redis 管道命令
redis-cli --pipe 可以大量插入数据,也可以从文件中批量插入数据。对于我们要手动为系统缓存一些数据到 Redis 时,可以通过数据库进行查询,查询后通过管道来进行导入
[root@VM_0_4_centos ~]# cat cmd.txt | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 5
WATCH 命令是一个乐观锁(optimistic locking)
功能: EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
每个 Redis 数据库都保存着一个 watched_ keys 字典
typedef struct redisDb {
// ...
//正在被WATCH 命令监视的键
dict *watched_keys;
// ...
} redisDb;
监视机制的触发
所有对数据库进行修改的命令,比如 SET、LPUSH、SADD、ZREM、DEL、FLUSHDB 等等,在执行之后都会调用 multi.c/touchWatchKey 函数对 watched_keys 字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么 touchWatchKey 函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。
判断事务是否安全
当服务器接收到一个客户端发来的 EXEC 命令时,服务器会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识来决定是否执行事务:
事务的 ACID 性质
事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失
因为 Redis 的事务不过是简单地用队列包裹起了一组 Redis 命令,Redis 并没有为事务提供任何额外的持久化功能,所以
Redis 事务的耐久性由 Redis 所使用的持久化模式决定:
先说下使用 Lua 脚本的好处:
减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
原子操作。redis 会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
复用。客户端发送的脚本会永久存在 redis 中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
Redis 从 2.6 版本开始引入对 Lua 脚本的支持,通过在服务器中嵌入 Lua 环境,Redis 客户端可以使用 Lua 脚本,直接在服务器端原子地执行多个 Redis 命令。
redis> EVAL "return 'hello world'" 0
"hello world"
而使用 EVALSHA 命令则可以根据脚本的 SHA1 校验和来对脚本进行求值,但这个命令要求校验和对应的脚本必须至少被 EVAL 命令执行过一次:
创建并修改 lua 环境
Redis 服务器创建并修改 Lua 环境的整个过程由以下步骤组成:
lua 脚本通信步骤
EVAL 命令的执行过程可以分为以下三个步骤:
Lua 环境协作组件
lua_scripts 字典
除了伪客户端之外,Redis 服务器为 Lua 环境创建的另一个协作组件是 lua_scripts 字典:
struct redisServer {
// ...
dict *lua_scripts;
// ...
};
Redis 的 SORT 命令可以对列表键、集合键或者有序集合键的值进行排序。转换的思想,行转列
redis> SORT key
实现原理
typedef struct _redisSortObject {
//被排序键的值
robj *obj;
//权重
union {
//排序数字值时使用
double score;
//排序带有BY 选项的字符串值时使用
robj *cmpobj;
} u;
} redisSortObject;
通过使用 ALPHA 选项,SORT 命令可以对包含字符串值的键进行排序:
SORT <key> ALPHA
在默认情况下,SORT 命令使用被排序键包含的元素作为排序的权重,元素本身决定了元素在排序之后所处的位置。例如,排序 fruits 集合所使用的权重就是"apple"、“banana” 、"cherry"三个元素本身:
另一方面,通过使用 BY 选项,SORT 命令可以指定某些字符串键,或者某个哈希键所包含的某些域(fheld) 来作为元素的权重,对一个键进行排序。
例如,以下这个例子就使用苹果、香蕉、樱桃三种水果的价钱,对集合键 fruits 进行了排序:
redis> MSET apple- -price 8 banana-price 5.5 cherry-price 7
OK
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
Redis 提供了 SETBIT、GETBIT、 BITCOUNT、BITOP 四个命令用于处理二进制位数组(bit array,又称“位数组”)
备注重点
计算汉明距离
以下是调用 swar (bitarray) 的执行步骤:
uint32_t swar(uint32_t i) {
//步骤1
i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
//步骤2
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
//步骤3
i = (i & 0x0F0F0F0F) + ((i >> 4) & 0x0F0F0F0F);
//步骤4
i = (i*(0x01010101) >> 24);
return i;
}
总结
Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。服务器配置有两个和慢查询日志相关的选项:
slowlog-log-slower-than选项指定执行时间超过多少微秒(1 秒等于 1 000 000 微秒)的命令请求会被记录到日志上。
slowlog-max-len 选项指定服务器最多保存多少条慢查询日志。服务器使用先进先出的方式保存多条慢查询日志,当服务器存储的慢查询日志数量等于 slowlog-max-len 选项的值时,服务器在添加一条新的慢查询日志之前,会先将最旧一条慢查询日志删除。
一般情况下,我们都是通过客户端连接 Redis 服务器,然后发送命令给 Redis 服务器,Redis 服务器会把每个客户端发来的命令缓存入一个队列,然后逐个进行执行,最后再把结果返回给客户端。而我们这里的慢查询指的就是“执行命令”的那部分。而非网络 I/O 或者 命令排队的问题。
命令
#获取慢查询日志
slowlog get
#清除慢查询日志
slowlog reset
慢查询日志的保存
struct redisServer {
// ...
//下一条慢查询日志的ID
long long slowlog_entry_id;
//保存了所有慢查询日志的链表
list *slowlog;
//服务器配置slowlog-log-slower-than 选项的值
long long slowlog_log_slower_than;
//服务器配置slowlog-max-len 选项的值
unsigned long slowlog_max_len;
// ...
};
typedef struct slowlogEntry { //唯一标识符 long long id; //命令执行时的时间,格式为UNIX 时间戳 time_t time; //执行命令消耗的时间,以微秒为单位 long long duration; //命令与命令参数 robj **argv; //命令与命令参数的数量 int argc; } slowlogEntry;
通过执行 MONITOR 命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息:
每当一个客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外, 还会将关于这条命令请求的信息发送给所有监视器
redis> MONITOR
OK
def MONITOR():
# 打开客户端的监视器标志
client.flags |= REDIS_MONITOR
# 将客户端添加到服务器状态的monitors 链表的末尾
server.monitors.append(client)
# 向客户端返回OK
send_reply("OK")
Redis 是一个纯内存的 KV 数据库,对于内存使用做了相当多的优化,比如:
同时 Redis 是个单线程数据库,意味着包括读取解析客户端命令、处理命令、回复响应、大部分后台任务都要在一个线程中完成,这就要求任何步骤都不能造成长时间的阻塞,由此造成了 Redis 独有的一些处理方式:
并不是所有的接口都适合增加缓存,但是对于类似字典表中的数据我们完全可以进行缓存,还有一些不经常变化的数据也可以进行缓存。
O(1)
;一个并发访问量比较大的 key 在某个时间过期,导致所有的请求直接打在 DB 上。
解决方案
缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如 DB)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力,就叫做缓存穿透。
避免
Bitmap
中,查询时通过该 bitmap 过滤。缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
避免
对热 key 的处理,最关键的是对热点 key 的监控,可以从这些端来监控热点 key:
只要监控到了热 key,对热 key 的处理就简单了:
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
这个问题在 Redis 2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行。
Redisson 分布式锁,就是对唯一管理,获取到了就获取到了锁,没有得到就没获取锁,不能执行,当然了,还涉及到锁的释放,超时等
set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole
分布式锁
redlock
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
Redis 主从集群切换数据丢失问题如何应对
对于 Redis 主节点与从节点之间的数据复制,是异步复制的,当客户端发送写请求给 master 节点的时候,客户端会返回 OK,然后同步到各个 slave 节点中。如果此时 master 还没来得及同步给 slave 节点时发生宕机,那么 master 内存中的数据会丢失;要是 master 中开启持久化设置数据可不可以保证不丢失呢?答案是否定的。在 master 发生宕机后,sentinel 集群检测到 master 发生故障,重新选举新的 master,如果旧的 master 在故障恢复后重启,那么此时它需要同步新 master 的数据,此时新的 master 的数据是空的(假设这段时间中没有数据写入)。那么旧 master 中的数据就会被刷新掉,此时数据还是会丢失。
首先我们需要理解集群的脑裂现象,这就好比一个人有两个大脑,那么到底受谁来控制呢?在分布式集群中,分布式协作框架 zookeeper 很好的解决了这个问题,通过控制半数以上的机器来解决。那么在 Redis 中,集群脑裂产生数据丢失的现象是怎么样的呢?
假设我们有一个 redis 集群,正常情况下 client 会向 master 发送请求,然后同步到 salve,sentinel 集群监控着集群,在集群发生故障时进行自动故障转移。此时,由于某种原因,比如网络原因,集群出现了分区,master 与 slave 节点之间断开了联系,sentinel 监控到一段时间没有联系认为 master 故障,然后重新选举,将 slave 切换为新的 master。但是 master 可能并没有发生故障,只是网络产生分区,此时 client 任然在旧的 master 上写数据,而新的 master 中没有数据,如果不及时发现问题进行处理可能旧的 master 中堆积大量数据。在发现问题之后,旧的 master 降为 slave 同步新的 master 数据,那么之前的数据被刷新掉,大量数据丢失。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03YED7l1-1672365181697)(http://qinyingjie.top/blogImg/793c2d5f39382a84aafe9703e3c36f55259d5c89.png@942w_432h_progressive.webp)]
在了解了上面的两种数据丢失场景后,我们如何保证数据可以不丢失呢?在分布式系统中,衡量一个系统的可用性,我们一般情况下会说 4 个 9,5 个 9 的系统达到了高可用(99.99%,99.999%,据说淘宝是 5 个 9)。对于 redis 集群,我们不可能保证数据完全不丢失,只能做到使得尽量少的数据丢失。
在 redis 的配置文件中有两个参数我们可以设置:
min-slaves-to-write 1 min-slaves-max-lag 10
min-slaves-to-write 默认情况下是 0
min-slaves-max-lag 默认情况下是 10
以上面配置为例,这两个参数表示至少有 1 个 salve 与 master 的同步复制延迟不能超过 10s,一旦所有的 slave 复制和同步的延迟达到了 10s,那么此时 master 就不会接受任何请求。
我们可以减小 min-slaves-max-lag 参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往 master 中写入数据。
那么对于 client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入 master 来保证数据不丢失;也可以将数据写入 kafka 消息队列,隔一段时间去消费 kafka 中的数据。
通过上面两个参数的设置我们尽可能的减少数据的丢失,具体的值还需要在特定的环境下进行测试设置。
后端程序实现的时候,第一个想到的肯定是排序算法,把要进行排行的数据收集起来,使用排序算法排好序,第一个元素排名为 1,依次类推。我们知道,排序算法,使用快排的平均时间复杂度为 O(nlogn), 获取玩家的排名,需要遍历计算排名,时间复杂度为 O(n)。 那么更新玩家积分并获取玩家最新排名,这个操作的时间复杂度为 O(nlogn) + O(n), 对于少量玩家排行榜系统来说,是可以接受的。但是对于 100 万,甚至 1000 万玩家的排行榜系统来说,每次更新都要重新排序,显然不可接受,我们需要更快的算法。
更快的算法,基本思路就是空间换时间,摒除每次都将所有数据重新排序的思路,改为部分数据调整,Redis 的 Sorted Set 数据结构使用了 跳表 和 哈希表 结合的方式,更新数据的时间复杂度为 O(logn), 获取排行的时间复杂度为 O(logn), 即使数据量很大,也能高效排序。
//缓存淘汰策略
#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)
#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_VOLATILE_TTL (2<<8)
#define MAXMEMORY_VOLATILE_RANDOM (3<<8)
#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_ALLKEYS_RANDOM ((6<<8)|MAXMEMORY_FLAG_ALLKEYS)
#define MAXMEMORY_NO_EVICTION (7<<8)
一共有 8 种:
1、MAXMEMORY_VOLATILE_LRU:从设置了过期时间的键中根据 LRU 淘汰
2、MAXMEMORY_VOLATILE_LFU:从设置了过期时间的键中根据 LFU 淘汰
3、MAXMEMORY_VOLATILE_TTL:从设置了过期时间的键中根据最长生存时间淘汰
4、MAXMEMORY_VOLATILE_RANDOM:从设置了过期时间的键中任意选择键淘汰
5、MAXMEMORY_FLAG_LRU:从所有键中根据 LRU 淘汰
6、MAXMEMORY_FLAG_LFU:从所有键中根据 LFU 淘汰
7、MAXMEMORY_ALLKEYS_RANDOM:从所有键中任意选择键淘汰
8、MAXMEMORY_NO_EVICTION:不进行键淘汰,如果内存不够,直接 OOM
从资源使用角度来看,包含的知识点如下:
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用 keys
指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan
指令,scan
指令可以无阻塞的提取出指定模式的 key
列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys
指令长。
RedisTemplate 和 StringRedisTemplate 的区别
使用 RedisTemplate 和 StringRedisTemplate 之所以有所差异,**是因为 StringRedisTemplate 在对 Key 和 Value 进行序列化时,都使用字符串的方式进行序列化。**因此,我们选择 StringRedisTemplate 会方便一些。
三个经典的缓存模式
缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。一般我们是如何使用缓存呢?有三种经典的缓存模式:
Cache-Aside Pattern
Cache-Aside Pattern,即旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
读流程
写流程
更新的时候,先更新数据库,然后再删除缓存。
Read-Through/Write-Through(读写穿透)
Read/Write Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。
Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新
Write behind (异步缓存写入)
Write behind跟Read-Through/Write-Through有相似的地方,都是由Cache Provider
来负责缓存和数据库的读写。它两又有个很大的不同:Read/Write Through是同步更新缓存和数据的,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。
这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。但是它适合频繁写的场景,MySQL 的InnoDB Buffer Pool 机制就使用到这种模式。
延迟双删
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
删除缓存重试机制
不管是延时双删还是Cache-Aside 的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~
删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制
读取 biglog 异步删除缓存
重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实,还可以通过数据库的 binlog 来异步淘汰 key。
为了满足Redis
高性能的要求,Redis
特地设计了RESP
(全称REdis Serialization Protocol
)协议,用来作为Redis
客户端与服务端的通讯协议,RESP
协议有以下优点实现简单,解析高效,可读性好
注意:RESP
底层用的连接方式还是TCP
,RESP
只定义了客户端与服务端的数据交互格式
RESP 数据类型
在RESP
中,总共定义了5
种数据类型,分别是Simple String
、Errors
、Intergers
、Bulk Strings
和Arrays
,第一个字节与数据类型的映射关系如下:
Simple Strings
类型,第一个字节为+
Errors
类型,第一个字节为-
Integers
类型,第一个字节为:
Bulk Strings
类型,第一个字节为$
Arrays
类型,第一个字节为*
Remote Dictionary Server
NoSQL 是指非关系型数据库 ,非关系型数据库和关系型数据库两者存在许多显著的不同点,其中最重要的是 NoSQL 不使用 SQL 作为查询语言。其数据存储可以不需要固定的表格模式,一般都有水平可扩展性的特征。
NoSQL 主要有如下几种不同的分类:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。