Redis(Remote Dictionary Server),即远程字典服务。是一个开源的、使用C语言编写、支持网络、可基于内存亦可持久化的日志型、key-value数据库,并提供多种语言的API。
Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis是单线程(因为全部数据在内存中,使用单线程可以取消线程切换时的上下文切换资源消耗)的,基于内存操作,CPU不是redis的性能瓶颈,他的瓶颈在于机器的内存和网络带宽。
Redis键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis中键值和值都是用SDS(Simple Dynamic String)
存储。string同C语言一样,末尾保存一个’\0’,但是不计算在len中。
struct sdshdr{
int len; // 记录buf数组中已使用字节的数量,也就是SDS中字符串的长度
int free; // 记录未使用字节的数量
char buf[]; // 字节数组
};
select num # 默认有16各数据库,num为0-15
keys * # 查看数据库所有的key
flushdb # 清空当前库
flushall # 清空全部的数据库
KEYS * # 查看所有的key
SET _key _value # 设置key
GET _key # 获取key的value
EXISTS _key # 判断key是否存在
MOVE _key _num # 将指定key移动到num号数据库
EXPIRE _key _sec # 设置key过期时间,单位是秒
TTL _key # 查看key的剩余时间
TYPE _key # 查看key的类型
如果您想尽快回答并且您的负载低于redis-server峰值性能,那么避免流水线操作可能是最佳选择.但是,如果您希望能够处理更高的吞吐量,那么您可以处理请求的管道.响应可能需要更长时间,但您可以在某些硬件上处理更多请求.
两者都是非关系型内存键值数据库,主要有以下不同:
网络IO模型:Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll,kqueue和select,对于单存只有IO操作来说,单线程可以将速度优势发挥到最大;Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,主线程监听网络连接,接受请求后,将连接描述字pipe传递给worker线程,进行读写IO,网络层使用libevent封装的事件库,多线程模型可以发挥多核作用,但是引入了cache coherency和锁的问题。
**数据类型:**Redis支持五种不同的类型,可以更灵活的解决问题;Memcached仅支持字符串类型
**数据持久化:**Redis支持两种持久化策略(RDB快照和AOF日志);Memcached不支持持久化
**分布式:**Redis Cluster实现了对分布式的支持;Memcached不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
内存管理机制:
事件循环结构体aeEventLoop:事件循环结构体维护 I/O 事件表,定时事件表和触发事件表。
redis 的主函数中调用 initServer() 函数从而初始化事件循环中心(EventLoop),它的主要工作是在 aeCreateEventLoop() 中完成的。
事件注册:文件 I/O 事件注册主要操作在 aeCreateFileEvent() 中完成。aeCreateFileEvent() 会根据文件描述符的数值大小在事件循环结构体的 I/O 事件表中取一个数据空间,利用系统提供的 I/O 多路复用技术监听感兴趣的 I/O 事件,并设置回调函数。
准备监听的工作:redis 提供了 TCP 和 UNIX 域套接字两种工作方式。以 TCP 工作方式为例,listenPort() 创建绑定了套接字并启动了监听
为监听的套接字注册事件:initServer() 为所有的监听套接字注册了读事件(读事件表示有新的连接到来),响应函数为 acceptTcpHandler() 或者 acceptUnixHandler()。
进入事件循环:发生在 aeProcessEvents() 中:
aeApiPoll() 调用了 select() 进入了监听轮询。aeApiPoll() 的 tvp 参数是最小等待时间,它会被预先计算出来,它主要完成:
接下来的操作便是执行相应的回调函数,先处理 I/O 事件,再处理定时事件。
初始化:redis 在启动做了一些初始化逻辑,比如配置文件读取(initserverconfig),数据中心初始化,网络通信模块初始化等(initserver),待所有初始化任务完毕后,便开始进入事件循环等待请求(aemain)。
redis 注册了回调函数 acceptTcpHandler(),当有新的连接到来时,这个函数会被回调
获取客户端的数据:readQueryFromClient() 则是获取来自客户端的数据,接下来它会调用 processInputBuffer() 解析命令和执行命令,对于命令的执行,调用的是函数 processCommand()。
简单动态字符串(simple dynamic string),redis中使用sds作为默认字符串。
struct sdshdr{
// 1 bytes
uint8_t len; // 字符串长度
// 1 bytes
uint8_t free; // buf数组中未使用的字节数量
// 1 bytes
unsigned char flags;
char buf[]; // 字符串数组,最后有一个隐式的'\0'
};
双端,无环,带有表头指针和表尾指针,带链表长度计数器。
多态:链表节点使用void*指针来保存节点值,并且通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
typedef struct listNode{
struct listNode *pre;
struct listNode *next;
void* value;
};
typedef sturct list{
listNode *head;
listNode *tail;
unsigned long len;
void *(*dup) (void *ptr); // 节点值复制函数
void (*free) (void *ptr); // 节点值释放函数
int (*match) (void *ptr, void *key); // 节点值对比函数
}list;
dictht
是一个散列表结构,使用拉链法解决哈希冲突。
// 一个哈希表结构,每个字典有两个这样的结构
typedef struct dictht{
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long used; // 该哈希表已有节点的数量
}dictht;
// 哈希表节点结构
typedef struct dictEntry{
void *key; // 键
union{ // 值
void *val;
uint64_t u64;
int64_t s64;
double d;
}v;
struct dictEntry *next; // 指向下一个哈希表节点,形成链表,用来解决键冲突
}dictEntry;
Redis
的字典dict
包含两个哈希表dictht
,这是为了方便进行rehash
操作,在扩容时,将其中一个dictht
上的键值对rehash
到另一个dictht
上面,完成之后释放空间并交换两个dictht
的角色。
typedef struct dict{
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表,一般只使用ht[0],ht[1]是用来rehash的
long rehashidx; // 记录rehash目前的进度,如果没有进行rehash他的值为-1
unsigned long iterators;// 当前运行的迭代器的数量
}dict;
hash = dict->type->hashFunction(key);
index = hash & index->ht[0].sizemask;
Redis的哈希表使用链地址法解决哈希冲突,每个哈希表节点上都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单项链表连接起来。
程序总是将新节点添加到链表的表头为止(复杂度为O(1)),排在其他已有节点的前面。
当哈希表中键值对主键增多或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表键值对数量太多或者太少,程序需要对哈希表的大小进行相应的扩展或者收缩,也就是rehash操作。
rehash步骤:
ht[0].used*2的2^n
(会考虑redis是否正在bgsave,但是如果过多也会强制扩容)ht[0].used的2^n
(缩容条件:小于10%,不会考虑是否正在besave)rehash操作不是一次性完成的,而是采用渐进方式,这是为了避免一次性执行过多的rehash操作给服务器带来过大的负担。
渐进式rehash步骤:
rehashidx
,并将其设置为0,表示渐进式rehash开始采用渐进式rehash会导致字典中的数据分散在两个dictht上,因此对字典的查找操作也需要到对应的dictht去执行。
添加操作之后保存到ht[1]中,可以保证ht[0]中的键值对只减不增。
是有序集合的底层实现之一。是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
Redis中只有一个地方用到了跳跃表:有序集合。
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist
两个结构定义,其中zskiplistNode
结构用于表述跳跃表节点,zskiplist
结构用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头结点和表尾节点的指针等等。
typedef struct zskiplistNode{
// 层
struct zskiplistLevel{
// 前进指针
struct zskipListNode *forward;
// 跨度,用于计算一个元素的排名
unsigned int span;
}level[];
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
}zskiplistNode;
typedef struct zskiplist{
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
}zskiplist;
在查找中,从上层指针开始查找,找到对应的区间之后再到下一层取查找。
与红黑树等平衡树相比,跳跃表具有以下优点:
从当前的最高层开始,后继比待查关键字大,下移;比待查关键字小,右移。
是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
数据结构:intset.h/intset
typedef struct intset{
uint32_t encoding; // 元素的编码方式
uint32_t length;
int8_t contents[]; // 保存元素的数据
}intset;
是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
struct ziplist{
int32 zlbytes; // 整个压缩列表字节数
int32 zitail_offset;// 最后一个元素的偏移量
int16 zllength; // 元素个数
T[] entries; // 元素内容
int8 zlend; // 结束标志,恒为0xFF
};
struct entry{
int prevlen; // 上一个entry的字节长度,如果长度小于254就用一个字节表示;如果超出就用5个字节表示
int encoding; // 元素类型编码,通过前缀位识别具体存储的数据形式
optional byte[] content; // 元素内容
};
为了支持双向遍历,加入了最后一个元素的偏移量,同时在entry结构体中加入了上一个entry的字节长度。
增加元素时,因为ziplist都是紧凑存储,没有冗余空间意味着插入一个新元素都要调用realloc扩展内存。
对ziplist结构的改进,在存储空间上更加节省,结构上也比ziplist更精简。
struct listpack{
int32 total_bytes; // 占用的总字节
int16 size; // 元素个数
T[] entries; // 紧凑排列的元素列表
int8 end; // 0xFF
};
struct lpentry{
int encoding;
optional byte[] content;
int length;
};
因为在lpentry结构体的内部,length放置在了尾部,并且存储的是当前元素的长度,所以可以省去altail_offset
来标记最后一个元素的位置,这个位置可以通过total_bytes字段和最后一个元素的长度字段计算出来。
由于链表list的附加空间相对太高,prev和next指针就要占用16字节,另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。
quicklist是ziplist和linkedlist的结合体,他将linkedlist按段切分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针串接起来。
struct ziplist{
};
struct ziplist_compressed{
int32 size;
byte[] compressed_data;
};
struct quicklistNode{
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩链表
int32 size; // ziplist的字节总数
int16 count; // ziplist中的元素数量
int2 encoding; // 存储形式2bit,是按照原生字节数组还是LZF压缩存储
...
};
struct quicklist{
quicklistNode *head;
quicklistNode *tail;
long count; // 元素总数
int nodes; // ziplist节点的个数
int compressDepth; // LZF算法压缩深度
...
};
quicklist内部默认单个ziplist长度位8K字节,超出这个字节数,就会新起一个ziplist。ziplist长度由参数list-max-ziplist-size
决定。
quicklist默认压缩深度为0,也就是不压缩。为了支持快速的push/pop操作,quicklist的首尾两个ziplist不压缩,深度就是1。
一种有序字典树(基数树Radix Tree),按照key的字典序配列,支持快速定位、插入和删除操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KFXQRfY6-1602224447718)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/rax.png)]
Redis并没有直接使用上述数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。
Redis使用对象来表示数据库中的键和值,每次创建一个键值对时,至少会创建两个对象,一个对象为键对象,一个为值对象。
Redis每个对象都由一个redisObject
结构表示,该结构中和保存数据有关的三个属性分别为type,encoding,ptr
typedef struct redisObject{
unsigned type:4; // 4 bits
unsigned encoding:4; // 4 bits
unsigned lru:LRU_BITS; // 24 bits
int refcount; // 4 bytes
void *ptr; // 指向底层实现数据结构的指针,占用8 bytes
}robj;
REDIS_STRING, REDIS_LIST, REDIS_HASH, REDIS_SET, REDIS_ZSET
OBJECT ENCODING _key
可以获取键值的底层数据结构类型编码可以是int, raw, embstr
。
int
。否则将其按照字符串格式存储。raw
embstr
方式保存。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFAI1g3E-1602224447721)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/embstr+raw.png)]
embstr
存储方式是将RedisObject
对象头和SDS
对象连续存在一起,使用malloc
方法一次性分配。
raw
存储方式不存在一起,分两次分配空间,使用指针连接。
为什么是44字节作为分界点?
Redis内存分配器jemalloc/tcmalloc
等分配内存大小的单位都是2,4,8,16,32,64等,为了能容纳一个完整的embstr
对象,jemalloc
最少会分配32个字节的空间,如果字符再长一点,就是64字节的空间。如果更长就会按照raw方式分两次分配空间存储
当分配为64字节时,对象头占用16字节,SDS的len和free占用3字节,还有最后一个字符’\0’,所以最大为64-16-3-1=44字节。
编码使用quicklist
。
编码可以是ziplist
或者hashtable
。
ziplist
编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
hashtable
编码的哈希对象使用字典作为底层实现,哈希对象的每个键值对都是用一个字典键值来保存。
当哈希对象同时满足以下两个条件时,哈希对象使用ziplist,否则使用hashtable编码
编码可以是intset
或者hashtable
。
intset
编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
hashtable
编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,字典的值全被设为NULL。
当集合对象同时满足以下两个条件时,使用intset
编码:
有序集合对象的编码可以是ziplist
或者skiplist
。
ziplist
编码的压缩列表对象使用压缩列表来作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存。第一个节点保存元素的成员(member),第二个节点保存元素的分值(score)。
skiplist
编码的有序集合对象使用zset
结构作为底层实现,一个zset
结构同时包含一个字典和一个跳跃表。
typedef struct zset{
dict *dict;
zskiplist *zsl;
}zset;
zsl
跳跃表按照分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,跳跃表节点的score属性保存了元素的分值。通过跳跃表,可以对有序集合进行范围型操作。dict
字典为有序集合创建一个成员到分值的映射,字典的每个键值都保存了一个集合元素:字典的键保存了元素的成员,字典的值保存元素的分值。通过字典,可以以O(1)复杂度查找给定成员的分值。zset
结构同时使用zsl
和dict
来保存有序集合元素,但是这两种数据结构都会通过指针来共享相同元素的成员和分值。ziplist
编码:
数据类型 | 可以存储的值 | 操作 |
---|---|---|
STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
LIST | 列表 | 从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素 |
SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 |
ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
APPEND _key _string # 向key的value后追加字符串,如果不存在相当于set key
STRLEN _key # 获取key的长度
INCR _key # key的value自加1
DECR _key # key的value自减1
INCR _key _num # key的value自加num
DECR _key _num # key的value自减num
GETRANGE_key start end # 截取[start,end]范围的字符串,-1表示最后一个
SETRANGE_key offset string # 替换从offset开始的之后的字符串
SETEX _key _time _string # 设置带有过期时间的key
SETNX _key _string # 不存在时设置
MSET _key _value _key _value # 批量设置key
MGET _key _key # 批量获取key
MSETNX _key _value # 批量设置不存在的,原子性操作,要么全部成功,要么一起失败
RPUSH _list _item # 从list右边插入value,如果没有list就创建
LPUSH _list _item # 从左边插入
RPOP _list # 从右边删除一个
LPOP _list # 从左边删除一个
LRANGE _list start end # 获取list中范围value
LINDEX _list _index # 获取指定位置的value
集合是通过哈希表实现的。集合中最大的成员数为2^32-1(即每个集合可存储40多亿个成员)
SADD _set _item # 集合中插入一个value,如果没有就创建
SMEMBERS_set # 获取集合所有value
SISMEMBER _set _item # 查看item是否在集合中
SREM _set _item # 移除元素
HSET _hash _key _value # 在_hash表中设置_key和_value
HGETALL _hash # 获取_hash表中所有键值对
HDEL _hash _key # 删除_hash表中指定键
HGET _hash _key # 获取指定键
每个元素都会关联一个double类型的分数,redis时通过分数为集合中的成员从小到大进行排序的。
有序集合成员是唯一的,但是分数是可以重复的。
ZADD _zset _score _mem # 向zset有序集合中添加成员
ZRANGE _zset start end WITHSCORES # 获取范围内成员,WITHSCORES参数表示同时获取分数
ZRANGEBYSCORES _zset start end WITHSCORES # 根据分数范围获取
ZREM _zset _mem # 移除成员
Redis服务器将所有的数据库保存在服务器状态redis.h/redisServer
的db数组中,db数组的每个项都是一个redis.h/redisDb
,每个redisDb结构代表一个数据库。
Redis中数据库的数量会根据dbnum的值进行创建,默认情况下dbnum值为16.
typedef struct redisDb {
dict *dict;
dict *expires;
int id;
} redisDb;
Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都有一个redis.h/redisDb
结构表示,其中redisDb结构中的dict字典保存了数据库中的所有键值对,这个字典称为键空间(key space)
因为数据库的键空间是一个字典,所有所有针对数据库的操作(添加,删除,更新,取值),实际上都是通过对键空间字典进行操作来实现的。
redisDb结构的expires字典保存了数据库中所有键的过期时间,这个字典称为过期字典。
redis.c/expireIfNeeded
函数实现,如果输入键未设置过期时间或者设置过期时间但未过期,不做动作;否则删除键。redis.c/activeExpireCycle
函数实现,分多次遍历每个数据库,从数据库中的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。其中current_db
记录了最后被检查的数据库,下次定期删除的时候从该数据库开始遍历。该功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
notify-keyspace-events
选项决定了服务器所发送通知的类型:
AKE
:发送所有类型的键空间通知和键空间事件通知AK
:发送所有类型的键空间通知AE
:发送所有类型的键事件通知K$
:只发送和字符串键有关的键空间通知El
:只发送和列表键有关的键事件通知该功能由void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid)
函数实现
可以对string进行自增自减运算,从而实现计数器的功能。
Redis这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
将热点数据放在内存中,设置内存的最大使用量以及淘汰缓存策略来保证缓存的命中率。
查找表和缓存类似,也是利用了redis快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
例如DNS记录就很适合使用redis进行存储。
List是一个双向链表,可以通过LPUSH和RPOP写入和读取信息。
不过最好使用rebbitmq等消息中间件。
可以使用redis来统一存储多台应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
在分布式场景下,无法使用单机环境的锁来对多个节点上的进程进行同步。
可以使用redis自带的SETNX命令实现分布式锁,除此之外,还可以使用官方提供的redlock分布式锁来实现。
Set可以实现交集、并集等操作,从而实现共同好友等功能。
ZSet可以实现有序性操作,从而实现排行榜等功能。
可以设置内存最大使用量,当内存使用量超出时,会实行数据淘汰策略。
Redis中有6中淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
作为内存数据库,处于对性能和内存消耗的考虑,redis的淘汰算法实际实现上并非针对所有key,而是抽样一部分并且选出被淘汰的key。
使用redis缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后使用allkeys-lru
淘汰策略,将最近最少使用的数据淘汰。
Redis是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
通常将服务器中的非空数据库以及他们的键值对统称为数据库状态。
将某个时间点的所有数据(RDB文件)都存放在硬盘上。
可以将快照复制到其他服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间很长。
BGSAVE
和SAVE、BGSAVE、BGREWRITEAOF
命令冲突。一个完整的RDB文件包含五部分:REDIS, db_version, databases, EOF, check_sum
REDIS
:RDB文件标识,存储的是REDIS五个字符db_version
:长度4个字节,一个字符串表示的整数,记录了RDB文件的版本号databases
:包含零个或者任意多个数据库,以及各个数据库中的键值对数据
EOF
:长度为1个字节,标志RDB文件正文内容结束check_sum
:8字节长度的无符号整数将写命令添加到AOF文件的末尾。(Append Only File),是通过保存Redis服务器所执行的写命令来记录数据库状态的。
使用AOF持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这时因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。
选项 | 同步频率 |
---|---|
always | 每个写命令都同步 |
everysec | 每秒同步一次 |
no | 让操作系统来决定何时同步 |
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
AOF持久化的实现可以分为**命令追加(append),文件写入(写入内存缓冲区),文件同步(sync,写入真实文件)**三个步骤。
aof_buf
缓存区的末尾。aof_buf
缓冲区中,所以在服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile
函数,考虑是否将缓冲区内容写入和保存到AOF文件里面。
flushAppendOnlyFile
函数的行为可以通过配置appendfsync
选项的值来决定,
always
时将缓冲区的内容写入并同步到AOF文件中,最安全的,但是效率最慢;everysec
将缓冲区内容写入到AOF文件中,如果上次同步AOF时间为一秒前,就对AOF文件进行同步,如果出现故障只会丢失1s内的数据,效率适当;no
将aof_buf
缓冲区内容写入到AOF文件,但是并不对其进行同步,何时同步由操作系统决定,安全性最低,效率和everysec差不多。可以使原本包含很多冗余命令的AOF文件减小很多,所以文件大小也会小很多,最后数据还原的结果是相同的。
Redis服务器是一个事件驱动程序。
服务器通过套接字和客户端(或者其他服务器)进行通信,文件事件就是对套接字操作的抽象。
Redis通过Reactor模式开发了自己的网络事件处理器,被称为文件事件处理器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e2Wti5oS-1602224447728)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/fileevent.png)]
Redis的IO多路复用程序的所有功能都是通过包装常用的select,epoll,evport和kqueue
这些IO多路复用函数库来实现的。
Redis在IO多路复用程序的实现源码中用#include
宏定义了相应的规则,程序会在编译的时候自动选择系统中性能最高的IO多路复用函数库来作为redis的IO多路复用的底层实现。
IO多路复用程序可以监听多个套接字的ae.h/AE_READABLE, ae.h/AR_WRITABLE
事件,分别对应:
AE_READABLE
事件。AE_WRITABLE
事件。当一个套接字同时可读可写时,服务器优先处理读,再处理写。
ae.c/aeCreateFileEvent
函数接受一个套接字描述符,一个事件类型,一个事件处理器作为参数。
networking.c/acceptTcpHandler
,用于对服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accept
函数的封装。
AE_READABLE
事件关联起来,当客户端使用sys/scoket.h/connect
函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE
事件,引发连接应答处理器执行networking,c/readQueryFromClient
,负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read
函数的包装。
AE_READABLE
事件关联起来,当客户端向服务器发送命令请求时,会产生AE_READABLE
事件,引发命令请求处理器执行。AE_READABLE
关联命令请求处理器networking.c/sendReplyToClient
,负责将服务器执行命令后得到的命令恢复通过套接字返回给客户端,具体实现为unistd.h/write
函数的包装
AE_WRITABLE
事件和命令回复处理器关联起来,当客户端准备好接收命令回复时,会产生该事件,引发命令回复处理器执行。服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
时间事件又分为:
时间事件主要由以下三个属性组成:
id
:服务器为事件事件创建的全局唯一ID(标识号),ID按照从小打到的顺序递增,新事件的ID号总是大于旧事件的。when
:毫秒精度的UNIX时间戳,记录了时间事件的到达时间。timeproc
:时间事件处理器,一个函数。一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值。
ae.h/AE_NOMORE
,该事件为定时事件:该事件到达一次就会被删除,之后不会到达。Redis将所有时间事件都放在一个无序链表(无序链表不是不按ID排序,而是说该链表不按when属性的大小排序)中。
通过遍历这个链表查找已经到达的时间事件,并调用相应的事件处理器。
因为链表没有按照when属性进行排序,所以需要遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理。
ae.c/aeCreateTimeEvent
函数接受一个milliseconds和一个时间事件处理器proc作为参数,将一个新的时间时间添加到服务器。
ae.c/aeDeleteTimeEvent
函数接受一个时间事件ID作为参数,然后从服务器中删除该ID对应的时间事件。
ae.c/aeSearchNearestTimer
函数返回到达事件距离当前事件最近的那个时间事件
ae.c/processTimeEvents
函数是时间事件的执行器,这个函数遍历所有已经到达的时间事件,并调用这些事件的处理器。
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件的调度和执行由ae.c/aeProcessEvents
函数负责:
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件,事实上这个部分是直接写在这个函数里面的
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
将aeProcessEvents
函数置于一个循环之中,加上初始化和清理函数,就构成了redis的服务器的主函数。
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
aeApiPoll
函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这种方法既可以避免服务器对时间事件进行频繁的轮询,也可以确保aeApiPoll
函数不会阻塞过长时间。Redis服务器是一个典型的一对多服务器程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
通多使用由IO多路复用技术实现的文件事件处理器,redis服务器使用单线程单进程的方式来处理命令请求,并于多个客户端进行网络通信。
对于每个域服务器进行连接的客户端,服务器都为这些客户端建立了相应的server.h/client
结构。主要内容有:
struct client{
int fd; // 客户端描述字
robj *name; // 客户端名字
sds querybuf; // 客户端请求缓存
int argc;
robj **argv; // 当前命令
struct redisCommand *cmd;
redisDb *db; // 指向当前选中的数据库的指针
int flags; // 客户端的标志值
list *reply; // 返回给客户端的对象
char buf[PROTP_REPLY_CHUNK_BYTES]; // 固定长度输出缓冲区
int bufpos; // 记录输出缓冲区输出目前已经使用的字节数量
list *reply; // 可变大小输出缓冲区
...
}
一般情况下客户端是没有名字的,但是为了让客户端的身份清晰,可以使用CLIENT setname
命令为客户端设置名字
记录了客户端的角色(role),以及客户端所处的状态。
REDIS_MASTER
,从服务器标志值为REDIS_SLAVE
REDIS_LUA_CLIENT
表示仅为处理Lua脚本命令的伪客户端REDIS_UNIX_SOCKET
表示服务器使用UNIX套接字来连接客户端客户端状态的输入缓冲区用于保存客户端发送的命令请求。
输入缓冲区的大小会根据输入内容动态的缩小或者扩大,但是不能超过1G,否则服务器会关闭这个客户端。
服务器将客户端发送的命令请求保存到输入缓冲区之后,会对命令请求的内容进行分析,并将得到的命令参数以及命令参数的个数分别保存到客户端状态的argv属性和argc属性。
argv
是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]是要执行的命令,之后的其他项是传给命令的参数argc
负责记录argv数组的长度struct redisCommand *cmd
:服务器从协议内容分析得到argv和argc属性后,服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。
执行命令得到的命令回复会被保存在客户端状态的输出缓冲区里,每个客户端都有两个输出缓冲区可用,一个的大小是固定的,一个大小是可变的。
客户端状态中的authenticated
属性记录客户端是否通过了身份验证,如果为0表示未通过身份验证,为1表示通过
AUTH
命令之外,客户端的其他命令都会被服务器拒绝执行time_t ctime; // 客户端创建的时间
time_t lastinteraction; // 最后一次互动的时间
time_t obuf_soft_limit_reached_time; // 客户端的空转时间,也即距离最后一次互动过去了多久
连接:通过网络连接和服务器进行连接的客户端,在客户端使用connect
函数连接到服务器时,服务器会调用连接事件的处理器,并为客户端创建相应的客户端状态,并将这个客户端状态添加到服务器状态结构clients链表的结尾。
关闭:
CLIENT KILL
命令的目标,会被关闭timeout
选项,那么客户端的空转时间超过这个值,会被关闭obuf_soft_limit_reached_time
属性记录客户端到达软性限制的起始时间,如果该属性持续超过服务器设定时长,客户端会被关闭;否则该值清零服务器在初始化时会创建负责执行lua脚本中包含的redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client
属性中,伪客户端在服务器运行的整个生命周期都是存在的。
**AOF文件的伪客户端:**服务器在载入AOF文件时,会创建用于执行AOF文件包含的reids命令的伪客户端,并在载入完成之后,关闭这个伪客户端。
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令产生的数据,并通过资源管理来维持服务器自身的运转。
例:SET KEY VALUE --->OK
SET KEY VALUE
SET KEY VALUE
转换为协议格式*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
当连接套接字因为客户端的写入而变得可读时,服务器调用命令请求处理器来执行以下操作:
struct redisCommand{
char *name; // 命令名称,比如"SET"
// typedef void redisCommandProc(client *c)
redisCommandProc *proc; // 函数指针,指向命令的实现函数,比如setCommand
int arity; // 命令参数的个数,用于检查命令请求是否正确。如果该值为-N,标识参数数量大于等于N
char *sflags; // 字符串形式的标识值,这个值记录了命令的属性
uint64_t flags; // 对标识分析得到的二进制标识
long long microseconds, calls; // 执行命令耗费的总时长,总共执行了多少次这个命令
};
// sflags
w // 写入命令,可能会修改数据库, 比如SET, RPUSH, DEL等
r // 只读命令,不会修改, 比如GET, STRLEN, EXISTS等
m // 该命令会占用大量内存,执行前先检查内存情况, 比如SET, APPEND,RPUSH等
a // 管理命令 比如SAVE,BGSAVE等
例如:SET命令的名字为"set",实现函数为setCommand
,参数个数为-3;标识为"wm
"
程序对输入的argv[0]
,在命令表中进行查找,命令表返回argv[0]
对应的redisCommand
结构,客户端状态的cmd指针会指向这个结构。
执行预备操作:
stop-writes-on-bgsave-error
,而且服务器将要执行的是一个写命令,那么服务器拒绝执行这个命令,并返回错误调用命令的实现函数
client->cmd->proc(client)
,被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里,之后实现函数还会为客户端的套接字关联命令回复处理器,负责后续回复操作。
执行后续操作
实现函数执行完后,还会执行一些后续操作:
将命令回复给客户端
关联命令回复处理器后,当客户端套接字变为可写状态时,服务器会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
当发送完成后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。
该函数默认每个100ms执行一次,负责管理服务器的资源,并保持服务器自身的良好运转。
服务器状态中的unixtime
和mstime
属性被用作当前时间的缓存。由于每100ms更新一次,所以该时间精度不高
服务器状态中的lruclock
属性保存了服务器的LRU时钟,也是服务器时间缓存的一种。
当服务器要计算一个数据库键的空转时间,程序会使用服务器的lruclock
属性记录的时间减去对象的lru
属性记录的时间,得出的结果就是这个对象的空转时间。
trackInstantaneousMetric
函数以每100ms一次的频率执行,这个函数的功能是以抽样计算的形式,估算并记录服务器在最近一秒钟处理的命令请求数量。估算值
服务器状态中的stat_peak_memory
属性记录了服务器的内存峰值大小。更新最大值
启动服务器时,Redis会为服务器进行的SIGTERM信号关联处理器sigtermHandler
函数,这个信号处理器负责在服务器接收到SIGTERM信号时,打开服务器状态的shutdown_asap
标识。
每次根据该标识决定是否关闭服务器。关闭之前会进行RDB持久化操作。
每次执行都会调用clientsCron
函数,会对一定数量的客户端进行以下两个检查
内次执行都会调用databsesCron
函数,对服务器中一部分数据库进行操作,删除其中的过期键,并在有需要时,对字典进行收缩操作。
在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。
服务器状态的aof_rewrite_scheduled
标识记录了服务器是否延迟了该命令。
服务器状态使用rdb_child_pid
和aof_child_pid
属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程pid,这两个属性可以用于检查这两个命令是否正在执行。
rdb_child_pid
:如果服务器没有在执行BGSAVE,该值为-1aof_child_pid
:如果服务器没有在执行BGREWRITEAOF,该值为-1[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLTnlgFJ-1602224447734)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/chijiuhua.png)]
如果开启了AOF持久化,并且AOF缓冲区内还有待写入的数据,那么此时将AOF缓冲区中的内容写入到AOF文件里面。
服务器会关闭那些输出缓冲区大小超出限制的客户端
cronloops
属性的唯一作用就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能
clients
链表,db
数组,server.pubsub_channels
字典,server.lua
,server.slowlog
慢查询日志属性。完成了服务器状态server的初始化之后,服务器需要载入rdb文件或者aof文件,并根据文件内容还原数据库的状态。
初始化的最后一步,服务器打印准备好进行连接之后开始执行服务器的事件循环(loop)
Redis中,可以通过执行SLAVEOF
命令或者设置slaveof
选项,让一个服务器去复制(replicate)另一个服务器,被复制的称为主服务器(master),进行复制的被称为从服务器(slave)。
Redis的复制功能分为两个操作:同步(sync)和命令传播(command propagate):
SYNC是一个非常耗费资源的操作:
主服务器将自己执行的写命令,发送给从服务器执行,这样可以保持一致状态。
为了解决旧版复制功能在处理断线重复情况时的低效问题,Redis使用PSYNC命令代替SYNC命令来执行复制时的同步操作。
PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:
有三个部分构成:
复制偏移量
执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:
通过对比复制偏移量,可以很容易的知道主从服务器是否处于一致状态。
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个**固定长度(fixed-size)先进先出(FIFO)**队列,默认大小为1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里。
当从服务器重新连接到主服务器时,从服务区会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个偏移量来决定服务器进行何种同步操作:
复制积压缓冲区的大小可以用second*write_size_per_second来估算。
服务器运行ID
每个Redis服务器,不论是主服务器还是从服务器,都会有自己的运行ID
运行ID在服务器启动时自动生成,由40个随机的16进制字符组成。
从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID发送给从服务器,从从服务器会将这个运行ID保存起来。
当从服务器断线并重新连接这个主服务器,从服务器将向当前连接的主服务器发送之前保存的运行ID;如果一致尝试执行部分重同步,如果不一致执行完整重同步操作。
如果从服务器从来没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时发送PSYNC ? -1
命令,主动请求主服务器进行完整重同步。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKXw3cIa-1602224447736)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/psync.png)]
通过向从服务器发送SLAVEOF
命令,可以让一个从服务器去复制一个主服务器:
SLAVEOF
复制功能实现的主要步骤:
masterhost
和masterport
属性里。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zZl2gP27-1602224447739)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/copyping.png)]
masterauth
选项,就进行身份验证;反之不进行身份验证masterauth
选项的值[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hG66MWTc-1602224447744)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/copyauth.png)]
REPLCONF listening-port
,向主服务器发送从服务器的监听端口号。slave_listening_port
属性中INFO replication
命令时输出端口号REPLCONF ACK
,参数为从服务器当前的偏移量,主要用来实现
Sentinel(哨兵)是Redis的高可用性解决方案:有一个或者多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,自动将下线主服务器属下的某个从服务器升级为主服务器,然后由新的主服务器代替下线的主服务器继续处理命令请求;并发送命令将余下的从服务器转转化为当前主服务器的从服务器。
当下线的主服务器重新上线之后,自动将其设置为当前主服务器的从服务器。
Sentinel本质就是一个运行在特殊模式下的Redis服务器。
redis-sentinel
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8np36AJO-1602224447746)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/sentinelfunc.png)]
sentinel.c/sentinelcmds
,普通服务器使用的是server.c/redisCommandTable
服务器初始化一个sentinel.c/sentinelState
结构(称为Sentinel状态),这个结构保存了服务器所有和Sentinel功能有关的状态。
struct sentinelState{
dict *masters; // 字典的键是主服务器的名字,值是一个指向sentinelRedisInstance结构的指针
...
}sentinel;
Sentinel状态中的masters字典记录了所有被Sentinel监视的主服务器的相关信息,其中:
sentinel.c/sentinelRedisInstance
结构,每个该结构代表一个被Sentinel监视的Redis服务器实例,这个实例可以是主服务器、从服务器,或者是另一个Sentinelmasters字典的初始化是根据Sentinel配置文件来进行的。
初始化的最后一步是创建连向主服务器的网络连接,Sentinel将称为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息。
对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:
__sentinel__:hello
频道Sentinel默认以每10秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。信息中一部分是主服务器自身的信息;另一部分是主服务器属下所有的从服务器的信息。
# Server
...
run_id:xxxxxxx(40位)
...
# Replication
role:master
...
slave0:ip=xxx,port=xxx,state=online,offset=xx,lag=0
slave1:ip=xxx,port=xxx,state=online,offset=xx,lag=0
...
# Other Sections
...
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会对这个新的从服务器进行相应的实例结构之外,Sentinel还会创建连接到这个从服务器的命令连接和订阅连接。
# Server
...
run_id:xxxxx
...
# Replication
role:slave
master_host:xxx
master_port:xxx
master_link_status:up // 主从服务器连接状态
slave_repl_offset:xxx // 从服务器复制偏移量
slave_priority:100 // 优先级
# Other Sections
...
默认情况下,Sentinel会以每2秒一次的频率通过命令连接向所有被监视的服务器发送以下格式的命令:
PUBLISH __sentinel__:hello "
其中s开头的信息为Sentinel本身的信息,m开头的信息为主服务器的信息。
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello
Sentinel对__sentinel__:hello
频道的订阅会一直持续到Sentinel与服务器连接断开为止。也就是说,对于每个和sentinel连接的服务器,Sentinel既可以通过命令连接向服务器的__sentinel__:hello
频道发送信息,也可以通过订阅连接从服务器的__sentinel__:hello
频道接收信息。
对于同监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的互相认知。
Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身之外,其他监视该主服务器的Sentinel的资料。
当一个Sentinel接收到其他Sentinel发来的信息时,目标Sentinel会从信息中分析并提取出一些参数:
根据提取出的主服务器参数,目标Sentinel会在自己的状态的masters字典中查找相应的主服务器实例结构,然后根据提取出的Sentinel参数检查主服务器实例结构中的sentinels字典,然后创建新实例结构或者更新实例。
每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线
当Sentinel将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线,他会同样监视这个主服务器的其他Sentinel进行询问,当Sentinel从其他Sentinel那里接收到足够数量(启动时设置的参数)的已下线判断之后,Sentinel就会判定为客观下线,并进行故障转移操作。
领头Sentinel用于对主服务器进行故障转移。
Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。
在系统运行正常的时候只有Leader和Follower两种状态的节点。一个Leader节点,其他的节点都是Follower。
Candidate是系统运行不稳定时期的中间状态,当一个Follower对Leader的的心跳出现异常,就会转变成Candidate,Candidate会去竞选新的Leader,它会向其他节点发送竞选投票,如果大多数节点都投票给它,它就会替代原来的Leader,变成新的Leader,原来的Leader会降级成Follower。
当需要故障转移的时候会在sentinel集群中选举出一个leader执行故障转移操作。Sentinel使用了Raft协议实现Sentinel间选举leader的算法。Sentinel集群运行过程中故障转移完成,所有Sentinel又会恢复平等。Leader仅仅是故障转移时出现的角色。
is-master-down-by-addr
命令请求选票,并带上自己的epochis-master-down-by-addr
命令,如果Sentinel当前epoch和Candidate传给他的epoch一样,说明它已经给别的candidate投过票,投过票的Sentinel在当前epoch内只能成为follower。选择完成后,领头Sentinel向被选中的从服务器发送SLAVEOF no one
命令,之后以一秒一次的频率向被升级的从服务器发送INFO命令,观察命令回复的角色信息。
向其他从服务器发送SLAVEOF
命令。
在旧主服务器实例中的角色属性将其转换为从服务器,在它重新上线后,向他发送SLAVEOF
命令。
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
一个Redis集群有多个**节点(node)**组成。使用CLUSTER MEET
命令连接各个节点:
CLUSTER MEET
向一个节点发送以上命令,可以让该节点同ip和port所指定的节点进行握手,握手成功之后,该节点就会将指定节点添加到该节点所在的集群中。
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled
配置选项是否为yes来决定是否开启服务器的集群模式。
节点会继续使用所有在单机模式下使用的服务器组件,比如文件事件处理器,时间事件处理器,RDB和AOF持久化等。
其中serverCron函数会调用集群模式特有的clusterCron函数,该函数会执行在集群模式下需要执行的常规操作。比如向集群中的其他节点发送Gossip消息,检查节点是否断线等。
节点还使用cluster.h/clusterNode,clusterLink,clusterState
结构保存集群模式状态。
每个节点都会使用一个clusterNode结构来保存自己的状态,比如节点创建时间,节点名字,节点IP地址和端口等,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的clusterNode结构。
clusterNode结构中的link属性是一个clusterLink结构,该结构保存了连接节点所需要的有关信息,比如套接字描述符,输入输出缓冲区等。
clusterState结构记录了当前节点的视角下,集群目前所处的状态,比如集群是在线还是下线,集群包含了多少节点,集群目前的配置纪元等
通过向节点A发送CLUSTER MEET
命令,客户端可以让接收命令的节点A将另一个节点B添加到集群里
收到命令的节点A将与节点B进行握手,来确认彼此存在:
clusterNode
结构,并添加到自己的clusterState.node
字典里clusterNode
结构,并添加到自己的集群节点字典中之后,节点A通过Gossip协议将节点B的信息传播给集群中的其他节点,让其他节点与节点B握手,经过一段时间,节点B会被集群中的所有节点认识。
Redis集群通过分片的方式来保存数据库中的键值时:集群的整个数据库被分为16384(2^14)个槽(slot),数据库中的每个键都属于这16384个槽中的其中一个,集群中的每个节点可以处理0或者16384个槽。(因为传输节点数据时用到了位图存储,一次大约2K,心跳包适中)
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(OK);相反如果有任何一个或者多个没有节点处理,就处于下线状态(fail)。
通过CLUSTER ADDSLOTS
命令,可以将一个或者多个槽指派给节点负责。
clusterNode
结构中的slots
和numslot
属性记录了节点负责处理哪些槽。其中slots
数组记录的二进制位信息,大小为16384/8。
节点除了会将自己负责的槽记录在属性中之外,海会将自己的slots
数组通过消息发送给集群中的其他节点,以此来告诉其他节点自己目前负责处理哪些槽。其他节点根据这些信息更新保存的其他节点结构中的slots
属性。
clusterState
结构中的slots
数组记录了每个槽的指派信息,大小为16384。
执行CLUSTER ADDSLOTS
命令中,需要先检查所有输入槽是否都处于未指派状态,然后再对他们进行指派操作。
在对数据库的16384个节点进行指派之后,集群就处于上线状态,当客户端向节点发送与数据库键有关的命令时,接收命令的节点计算要处理的键属于哪个槽CRC16(key)&16383
,并检查该槽是否指派给了自己:
MOVED
错误,指引客户端转向正确的节点,并向正确的节点再次发送命令。节点和单机服务器在数据库方面的一个区别是:节点只能使用0号数据库,单机没有这个限制。
除了将键值对保存在数据库里之外,节点还会用clusterState
结构中的slots_to_keys
rax树结构保存槽和键之间的关系。
Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会转到另一个节点。
重新分片操作是通过Redis的集群管理软件redis-trib
负责执行的,Redis提供了进行重新分片所需的所有指令,而redis-trib
则通过向源节点和目标节点发送命令来进行重新分片操作。
主要步骤为:
redis-trib
对目标节点发送CLUSTER SETSLOT IMPORTING
命令,让目标节点准备好导入属于槽slot的键值对。redis-trib
对源节点发送CLUSTER SETSLOT MIGRATING
命令,让源节点准备好迁移(migrating)slot键值对到目标节点。redis-trib
对目标节点发送CLUSTER GETKEYSINSLOT
命令,获得最多count个属于槽slot的键值对的键名。redis-trib
都向源节点发送MIGRATE 0
,将被选中的键原子的从源节点迁移到目标节点。redis-trib
向集群中任意一个节点发送CLUSTER SETSLOT NODE
命令,将槽id指派给目标节点,最终整个集群都会知道。在重新分片过程中,可能存在部分键值对保存在源节点中,另一部分在目标节点。此时仓客户端发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好属于正在被迁移的槽(保存在clusterNOde *migrating_slots_to[16384]
中)时:
两者都会导致客户端转向
Redis集群中由多个节点组成,主节点负责进行处理槽,每个主节点还可以有多个从节点,用于复制主节点,达到高可用的目的。
当发生主节点掉线后,集群自动从该主节点的从节点中选择一个作为主节点处理相应的槽。
CLUSTER REPLICATE
将其变为指定节点的从节点,并对其进行复制clusterState.nodes
字典中找到node_id
所对应节点的clusterNode
结构,并将自己的clusterState.myself.slaveof
指针指向这个结构,以此来记录这个节点正在复制的主节点。clusterState.myself.flags
中的属性,关闭REIDS_NODE_MASTER
,打开REDIS_NODE_SLAVE
,表示该节点已经变为从节点SLAVEOF
nodes值
)中的slaves
和numslaves
属性。ONLINE, PFAIL, FAIL
fail_reports
链表中。SLAVEOF no one
,成为新的主节点集群中的各个节点通过发送和接收消息(message)来进行通信,消息由消息头和消息体组成,消息主要包括以下5种:
MEET
消息:连接节点,使节点加入到集群中PING
消息:每1秒钟随机从已知节点列表中选出5个,对这5个节点最长没有发送过PING消息的节点发送PING消息,检测被选中阶段是否在线;除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout
选项设置时长的一半,那么节点A也会向节点B发送PING消息。PONG
消息:收到PING消息后返回PONG消息;另外,一个节点也可以通过广播自己的PONG消息来让集群中的其他节点立即刷新关于该节点的认知FAIL
消息:当一个主节点A判断另一个节点B进入FAIL状态,节点A向集群广播一条关于B的FAIL消息PUBLISH
消息:当节点收到一个PUBLISH命令,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有收到这条消息的节点都执行相同的PUBLISH命令。1.Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
2.Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3.Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
4.Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
**如果对方追问可不可以不用sleep呢?**list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
**如果对方追问能不能生产一次消费多次呢?**使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
**如果对方追问pub/sub有什么缺点?**在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
**如果对方追问redis如何实现延时队列?**我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。
对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
内连接是最常见的一种连接,也被称为普通连接,之链接匹配的行。它又分为等值连接(=)和不等值连接(between…and…)。
包含左,右两个表的全部行,不管另一边的表中是否存在与他们匹配的行
包含左边表的全部行(不管右边是否存在与他们匹配的行),以及右边表中全部匹配的行
包含右边表的全部行,以及左边表中全部匹配的行
笛卡尔乘积(所有可能的行对),交叉连接用于对两个源表进行纯关系代数的乘运算,它不使用连接条件来限制结果集合,而是将分别来自两个数据源中的行以所有可能的方式进行组合。
自然连接是一种特殊的等值连接,它要求两个关系中进行比较的分量必须是相同的属性组,并且在结果中把重复的属性列去掉;而等值连接不会去掉重复的属性列。
左自然连接,保留2个表的列(删除重复列),以左表为准,不存在匹配的右表列,值置为NULL
某个表和其自身连接,连接方式可以为内连接,外连接,交叉连接
基本思想:为数据库连接建立一个“缓冲池”,预先在池中放入一定数量的数据库连接管道,需要时从池子中取出管道进行使用,操作完毕后,再讲管道放入池子中,从而避免了频繁的向数据库中申请和释放资源带来的性能损耗。
MySQL没有真正意义上的表空间管理。MySQL的InnoDB包含两种表空间文件模式,默认的共享表空间和每个表分离的独立表空间。
在my.cnf
中添加innodb_file_per_table=1
可以从共享表空间切换到独立表空间。
默认的表空间方式。相对而言所有的数据都在一个(或几个)文件中,比较利于管理,而且在操作的时候只需要open这一个(或几个)文件即可,相对来说代价很低。
正常的使用缓存的流程为,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并且把查询到的对象放进缓存,如果数据库查询对象为空,则不放进缓存。
缓存穿透指查询一个数据库一定不存在的数据。比如发起id为’-1’的数据或者id特别大不存在的数据,这时的用户可能是攻击者,攻击会导致数据库压力过大。
缓存击穿是指缓存中没有但是数据库中有的数据(一般指缓存时间到期),这时由于并发用户很多,同时读缓存没读到数据,又同时去数据库读取数据,引起数据库压力瞬间增大,造成过大压力。
缓存雪崩是指缓存中数据有大批量到过期时间,而查询数据量巨大,引起数据库压力多大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查一条数据,缓存雪崩是指不同数据同时都过期了,进而都去查询数据库。
通过SQL语句,实现无账号登陆,甚至篡改数据库。主要原因是程序员可能在开发用户和数据库交互的系统时没有对用户输入的字符串进行过滤,转义,限制或者处理不严谨,导致用户可以通过精心构造的字符串去非法获取数据库中的数据。
相对而言对立表空间每个表都有独立的多个数据文件,而且做到了索引和数据的分离。多个小文件之间很方便的完成跨数据库甚至跨硬件的数据拷贝和迁移。相对来说灵活性很好。