NoSQL数据库,数据都在内存中,支持持久化,可以用作数据库,缓存,或者消息中间件,支持多种数据类型,一般作为缓存数据库辅助持久化的数据库。
对数据的操作是单线程原子性操作,并且支持主从和集群模式。
多路io复用 ,指的是同一个进程用一个线程处理多个IO数据流。
原理:多路Io复用是利用select、poll、epoll(不同的监控策略)可以同时监察多个流的IO事件的能力,在空闲的时候会把当前线程阻塞,当有一个或多个流有IO事件发生时,就从阻塞态中唤醒,处理就绪的流。
线程模型:redis基于reactor模式开发了自己的网络事件处理器,又称为文件事件处理器,由socket,I/O多路复用程序,文件事件分派器,事件处理器四部分组成。通过多路复用程序来监听多个socket,然后将事件压入队列,由分派器分派到相应的事件处理器进行处理。
优势:当处理的消耗对比IO几乎可以忽略不计时,可以处理大量的并发IO,而不用消耗太多CPU/内存。
新版 Redis 6.x
虽然io多路复用已经不错了,但是面临很多大键值的访问时,其中IO操作还是容易出现高延迟的问题,为了进一步优化,Redis 6.x把IO的部分做成允许多线程的模式。
注意这个IO部分只是处理网络数据的读写和协议解析,执行API命令仍然使用单线程。所以这个多线程并不会让redis存在并发的情况。
另外,多线程IO默认也是不开启的,需要再配置文件中配置
io-threads-do-reads yes
io-threads 4
String类型是Redis最基本的数据类型,一个key对应一个value,一个Redis中字符串value最多可以是512M
set
get
append
setnx
incr
将 key 中储存的数字值增1
只能对数字值操作,如果为空,新增值为1
decr
将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1
incrby / decrby
mset
mget
msetnx
setex
单键多值
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
lpush/rpush
lpop/rpop
rpoplpush
lrange
lrange mylist 0 -1 从0开始,-1表示获取所有
lindex
llen
linsert
lrem
场景: 队列 ,可重复集合
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
在符合条件的前提下(集合对象全部是整数,set-max-intset-entries 512)采用intset存储,否则采用dict存储,intset转dict的操作是不可逆的。
sadd
smembers
sismember
scard
srem
sinter
sunion
sdiff
场景: 去重 判存 集合间运算
Redis hash 是一个键值对集合,采用dict存储,在条件满足(未超过阈值)的情况下采用ziplist来进行存储(采用追加方式,先保存key在保存value),超过阈值后会由ziplist转向dict,且过程不可逆,所以尽量控制元素长度及数量。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map
场景: 经常有字段变化的对象、 键值对集合
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
底层使用ziplist或者skiplist存储,转换操作也是不可逆的。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的 。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
此类型可用于排序或者构建二级索引,比如按年龄段搜索姓名的时候,可以将分数设置为年龄根据zrangebyscore
来获取值,并且当分数相同时,redis会将值按字典序进行排序。
数据类型 | 适用场景 | 备注 |
---|---|---|
字符串(string) | 缓存;计数器 | 简单型的。如set stunum studentInfo。 计数器如限流 |
列表(list) | lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列) | 如阻塞队列,关注列表 |
哈希(hash) | 对象属性(尤其不定长的) | 如缓存studentInfo,hmset stunum stunum 1 stuname dinghaha age 18 |
集合(set) | 适用社交场景 | 赞/踩、粉丝、共同好友/喜好、推送 |
有序集合(zset) | 排行榜;优先队列; |
Redis 提供了2个不同形式的持久化方式。
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
#查看rdb文件命令
od -cx dump.rdb
save:save会阻塞redis的主进程,直到rdb文件创建完成,在整个过程中,redis不处理任何请求。
bgsave:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 fork过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
在conf配置文件中设置
save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
可以通过lastsave 命令获取最后一次成功执行快照的时间
执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义
RDB是整个内存的压缩过的Snapshot,RDB的数据结构
可以配置复合的快照触发条件,默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。
stop-writes-on-bgsave-error 当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes.
rdb文件由rdbSave函数创建,无论是save还是bgsave都会调用这个函数,可以在src里找到rdb.c进行查看
#save
void saveCommand(client *c) {
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) { //调用rdbSave
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
---------------------------------------------------------------------------------
#bgsave
void bgsaveCommand(client *c) {
int schedule = 0;
/* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
* is in progress. Instead of returning an error a BGSAVE gets scheduled. */
if (c->argc > 1) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (hasActiveChildProcess()) {
if (schedule) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"Another child process is active (AOF?): can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
"possible.");
}
} else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { //调用rdbSaveBackground
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
-------------------------------------------------------------------------------
#rdbSaveBackground
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
if ((childpid = redisFork()) == 0) {
int retval;
/* Child */
redisSetProcTitle("redis-rdb-bgsave");
redisSetCpuAffinity(server.bgsave_cpulist);
/*调用rdbSave*/
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
if (childpid == -1) {
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
return C_OK; /* unreached */
}
----------------------------------------------------------------------
#rdbSave
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp;
rio rdb;
int error = 0;
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
rioInitWithFile(&rdb,fp);
startSaving(RDBFLAGS_NONE);
if (server.rdb_save_incremental_fsync)
rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
stopSaving(1);
return C_OK;
werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF默认不开启,开启需配置appendonly yes。 可以在redis.conf中配置文件名称,默认为 appendonly.aof ,AOF文件的保存路径,同RDB的路径一致。
生成过程
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失或者说丢失数据的概率很小)
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好,这种模式数据写入及备份操作都由主进程进行,两个操作都会阻塞主进程
appendfsync everysec
理论上每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。实际执行过程中会丢长时间数据会由日志同步的快慢来决定,并且这边由于是由子线程来进行同步,不会阻塞主进程,但子线程完成的快慢会影响主进程的写入阻塞时长
判断逻辑如下
if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !force) {
/* With this append fsync policy we do background fsyncing.
* If the fsync is still in progress we can try to delay
* the write for a couple of seconds. */
if (sync_in_progress) {
if (server.aof_flush_postponed_start == 0) {
/* No previous write postponing, remember that we are
* postponing the flush and return. */
server.aof_flush_postponed_start = server.unixtime;
return;
} else if (server.unixtime - server.aof_flush_postponed_start < 2) {
/* We were already waiting for fsync to finish, but for less
* than two seconds this is still ok. Postpone again. */
return;
}
/* Otherwise fall trough, and go write since we can't wait
* over two seconds. */
server.aof_delayed_fsync++;
serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.");
}
}
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。同样的写入及备份都会由主进程进行,也都会阻塞主进程,其触发时机在当关闭Redis或者AOF的时候及系统的缓存被刷新的时候触发。
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
重写原理,如何实现重写
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。 这里子进程也会用到写时复制技术。
为了保证在重写期间的数据一致性,在重写期间会将命令同时追加到AOF重写缓冲区,在此次重写完成后,将AOF重写缓冲区的内容追加到新的AOF文件中并rename,在将AOF重写缓冲区的数据写入新的AOF文件和rename阶段主进程是阻塞的,这是原子性操作。
#define AOF_RW_BUF_BLOCK_SIZE (1024*1024*10) /* 10 MB per block */
typedef struct aofrwblock {
unsigned long used, free;
char buf[AOF_RW_BUF_BLOCK_SIZE];
} aofrwblock;
no-appendfsync-on-rewrite:
如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
触发机制,何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
官方推荐两个都启用。
如果对数据不敏感,可以选单独用RDB。
不建议单独用 AOF,因为可能会出现Bug。
如果只是做纯内存缓存,可以都不用。(谨慎,冗余出现雪崩)
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主,可以用作容灾快速恢复,但是他不能保证完全不丢失数据,因为这个主从节点之间的数据复制是异步的,在master写入完成并向客户端返回OK的时候挂掉的话,那么数据此时没有向slaver发送,会丢失数据
执行slaveof ip port
,成为相应节点从机,5.0之后使用replicaof ip port
执行slaveof no one
表示断开主从复制
可以设置sentinel哨兵模式,当主机挂掉后,自动从从机里选一个来当主机,但是这点应注意避免脑裂问题,可以设置超时时间,或者保证硬件的健康度来一定程度避免此问题。
同时我们可以通过设置以下两个参数来保证丢失尽可能少的数据
min-slaves-to-write 1
min-slaves-max-lag 10
如果要保证完全不丢数据的话可以在数据写入redis前增加一层备份层,可以先写入缓存或者落磁盘,也可以写入kafka这类的消息队列。
主从复制作用:
主从复制类型:
可以通过集群模式来进行水平扩容,通过在配置文件中开启集群模式,设置带副本的集群模式。
在集群模式下,当某一段插槽的主从节点全部挂掉,由cluster-require-full-coverage参数来决定集群状态,设置为yes时集群挂掉不可用,设置为no时,则这一段插槽不能使用,包括读和写
集群模式一共有16384个插槽,每个节点会负责相应的一部分。
//启动多个实例后,将其组合成集群模式
redis-cli --cluster create --cluster-replicas 1 ip1:6379 ip2:6380 ip3:6381 ip4:6389 ip5:6390 ip6:6391
//连接集群需要指定 -c ,来进行自动切换数据对应主机
redis-cli -c -p 6379
当需要不断的执行多条命令的时候,可以考虑使用流水线加速Redis的操作,当我们执行一次请求时,数据的往返会有时间消耗,并且执行的时间消耗很少,大部分时间都消耗在每次read()和write()函数的调用,上下文切换这些。这样即使我们的服务器能一秒处理十万个请求,但是我们的RTT(往返时延)比较长的话,比如RTT达到250毫秒,那么一秒也就只能处理4条请求。此时我们考虑使用流水线进行操作,他相当于将我们的命令进行一个批次处理,以减少RTT对我们执行效率的影响,此时,需要我们将大量的命令进行分批处理,因为此模式会在内存维护一个队列用以存储执行请求的返回结果,此时如果对批次不加以限制的话会非常耗费内存资源。
以下为本机测试效率对比
对比结果
----get Jedis----
共插入:[10000]条 ..
1,未使用PIPE批量设值耗时4746毫秒..
PIPE共插入:[10000]条 ..
2,使用PIPE批量设值耗时75毫秒 ..
共取值:[10000]条 ..
3,未使用PIPE批量取值耗时 4434毫秒 ..
PIPE共取值:[10000]条 ..
4,使用PIPE批量取值耗时24毫秒 ..
此模式简单来说就是通过一个中间件将服务端和客户端连接起来。
通过通道订阅的就是通过channel将两者连接起来进行一个解耦,服务端会有一个pubsub_channels属性,这个属性是一个字典,key为不同的channel,value为订阅了该频道的所有客户端。
通过模式来订阅的话,在服务端会有一个pubsub_patterns属性,此属性为链表结构,每一个节点都是保存的订阅的模式及订阅这个模式的客户端。
所以当服务端发送数据的时候,服务端会将数据发送给channel,然后再对channel对应的链表值进行遍历,将数据发送给所有订阅了该channel的客户端,接着,就会遍历模式对应的链表,匹配到对应模式后将数据发送给订阅该模式的客户端,所以,如果一个客户端既订阅了具体的频道,又订阅了包含该频道的模式,那么这个客户端将收到多次服务端发送的消息。
主要命令有
#发布消息
publish <channel> <message>
#订阅
subscribe <channel>
psubscribe <pattern>
#取消订阅
unsubscribe <channel>
punsubscribe <pattern>
能采用hash的情况下尽量采用hash结构
对于Hashes, Lists, Sets composed of just integers, and Sorted Sets这些结构,redis提供了一种特殊的编码形式,可以将数据进行压缩,相当于是在于CPU和内存之间的一种权衡
具体配置如下:
hash-max-ziplist-entries 512 //代表元素个数
hash-max-ziplist-value 64 //代表元素的最大字符数
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512
当满足我们设置的参数情况下,redis会自动帮我们进行一个特殊编码,使用的内存大概可以节省5-10倍,当值超过我们设定的范围时redis会自动帮我们转换为正常编码。
在获得锁的时候使用setnx获取,并且设置过期时间,时间到了就自动释放锁,对于过期时间的设定长短问题,可以根据处理逻辑的具体执行效率来设置一个合理的值,也可以采用守护线程的方式,检测到时间过去超时时间的多少比例时自动将过期时间后延,当然也可以设置一个单调递增的数字,在获取锁的时候附带发送送给客户端,在对数据库操作的时候会先对这个数字进行判断,对于小的数字请求不进行处理,以避免冲突问题。并且对于锁的值的设定需要随机且一定,用以后续释放锁的判断,防止产生多进程之间互相释放锁的操作。同时对于锁的操作都应该是原子性操作,以防止对锁的误操作。
此种方式在redis是单机部署的情况下,要防止redis故障停机
在集群或者主从模式下,在极端情况下无法保证锁的安全性(在当一个客户端刚获得锁的时候master宕机并且此时数据还未同步到slaver,然后slaver被选举为新的master后,就会被另一个客户端拿到新的锁,此时就会有两个客户端同时持有了锁)
此方案主要是解决redis故障时的锁安全问题
红锁方案获取锁的步骤:官网推荐N=5
针对于此种方案需要注意
如果我们有五个节点1,2,3,4,5
当此种情况发生时就会造成针对于共享资源同时有两把锁,针对于这一问题,redlock的提出者提出了延迟重启的策略,也就是一个节点崩溃后,先不重启他,会设置一个超过锁的过期时间的时间后重启,这样就可以避免此问题影响。
同时红锁算法比较依赖于系统时钟,当系统时钟出现跳跃的时候会直接影响到锁的安全性
redis实现分布式锁的常见问题及解决方案:
基于redis实现的分布式锁大致观点:在极端情况下无法保证结果的正确性,我们可以允许结果延迟,但是结果错误的话对于安全性来说是一个致命的问题。
所以整体而言的话,用redis来实现分布式锁,如果是用在提升效率上,比如只是为了不要做同样的工作,那单节点的就完全可以了,如果使用在对正确性要求很高的场景中,无论是单节点还是redlock都无法满足要求,在考虑时需要准备好兜底策略。
缓存穿透是指查询一个一定不存在的数据。由于缓存命不中时会去查询数据库,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决方案:
① 是将空对象也缓存起来,并给它设置一个很短的过期时间,最长不超过5分钟
② 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,就会造成缓存雪崩。
解决方案:
尽量让失效的时间点不分布在同一个时间点
缓存击穿,是指一个key非常热点,在不停的扛着大并发,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方案:
可以设置key永不过期
使用redis-benchmark -q
进行基准测试
PING_INLINE: 105485.23 requests per second
PING_BULK: 106044.54 requests per second
SET: 106837.61 requests per second
GET: 105152.48 requests per second
INCR: 102669.41 requests per second
LPUSH: 104166.67 requests per second
RPUSH: 106837.61 requests per second
LPOP: 104493.20 requests per second
RPOP: 107991.36 requests per second
SADD: 102145.05 requests per second
HSET: 107181.13 requests per second
SPOP: 108342.37 requests per second
LPUSH (needed to benchmark LRANGE): 91575.09 requests per second
LRANGE_100 (first 100 elements): 98039.22 requests per second
LRANGE_300 (first 300 elements): 99206.34 requests per second
LRANGE_500 (first 450 elements): 99900.09 requests per second
LRANGE_600 (first 600 elements): 103199.18 requests per second
MSET (10 keys): 112233.45 requests per second
基本可以看到,常用命令QPS基本在十万级
pipeline测试
不使用pipeline
redis-benchmark -t set,get,incr -n 1000000 -q
SET: 108014.69 requests per second
GET: 106780.57 requests per second
INCR: 102030.41 requests per second
使用pipeline
redis-benchmark -t set,get,incr -n 1000000 -q -P 10
SET: 865051.88 requests per second
GET: 907441.00 requests per second
INCR: 850340.12 requests per second
---------------------------------------------------------------
redis-benchmark -t set,get,incr -n 1000000 -q -P 20
SET: 1237623.75 requests per second
GET: 1398601.50 requests per second
INCR: 1658374.88 requests per second
----------------------------------------------------------------
redis-benchmark -t set,get,incr -n 1000000 -q -P 30
SET: 1838235.25 requests per second
GET: 1912045.88 requests per second
INCR: 1953124.88 requests per second
------------------------------------------------------------------
redis-benchmark -t set,get,incr -n 1000000 -q -P 50
SET: 2087682.62 requests per second
GET: 2380952.50 requests per second
INCR: 2283105.00 requests per second
---------------------------------------------------------------------
redis-benchmark -t set,get,incr -n 1000000 -q -P 100
SET: 2237136.50 requests per second
GET: 2645502.75 requests per second
INCR: 2624671.75 requests per second
redis是一个键值对数据库,每一个键值对都存储在dicEntry结构体中,其中key为这个键值对的键,是一个SDS,val为一个用redisObject包裹着的值,next则指向下一个键值对
typedef char* sds
/*
__attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法
*/
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 数据长度 */
uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/
unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/
//变长数据
char buf[];
};
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
redisObject
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
type对应type命令,保存对象类型
encoding对应object encoding命令,保存对象的底层数据结构,不同情况可能会有不同的结构
lru记录的是最后一次访问的时间
refcount 对应object refcount命令
ptr 模拟多态,可以存储任何类型的对象
Redis基于**RESP(Redis Serialization Protocol)**协议来完成客户端和服务端通信。RESP本质是一种文本协议,实现简单、易于解析。
我们常用的客户端结果显示是经过redis-cli.c文件进行转换而来,除了常用的客户端登陆外,我们也可以尝试使用nc命令来代替redis-cli
nc 127.0.0.1 6379
auth XXXX
select 15.....
其中用来管理redis命令的对象是通过redisCommand数据结构来管理的
typedef void redisCommandProc(client *c);
typedef int *redisGetKeysProc(struct redisCommand *cmd, robj **argv, int argc, int *numkeys);
struct redisCommand {
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
long long microseconds, calls;
};
arity : 用来限制命令个数,-N表示至少N个参数,包含命令本身
sflags : 字符串方式设置命令的属性,比如 w
表示这是个写入命令,r
表示这是个只读命令
flags : 将sflags字符串类型转为整型,多个属性之间用 |
运算,通过内部的populateCommandTable函数自动解析
redisCommandProc :函数指针类型,用来指向命令实现函数
microseconds : 命令执行耗时
calls : 命令执行总次数
命令运算规则
例:
struct redisCommand redisCommandTable[] = {
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}
}
char *name | set |
---|---|
redisCommandProc | 回调函数setCommand |
int arity | -3,表示至少三个参数(包括命令本身) |
char *sflags | wm,写入操作,同时执行前检查内存,内存不够拒绝执行 |
int flags | 0,表示没有任何额外设置 |
redisGetKeysProc | null,表示没有任何额外设置 |
int firstkey | 1,表示第一个参数是key |
int lastkey | 1,表示最后一个参数是key |
int keystep | 1,表示步长 |
microseconds | 0,表示执行该命令耗时初始化为0 |
calls | 0,表示执行该命令次数初始化为0 |
根据启动参数,判断是否是哨兵模式
server.sentinel_mode = checkForSentinelMode(argc,argv);
int checkForSentinelMode(int argc, char **argv) {
int j;
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
/* Check if we need to start in redis-check-rdb/aof mode. We just execute
* the program main. However the program is part of the Redis executable
* so that we can easily execute an RDB check on loading errors. */
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv,NULL);
else if (strstr(argv[0],"redis-check-aof") != NULL)
redis_check_aof_main(argc,argv);
解析命令行参数,如果设置配置文件,则将对其进行解析并覆盖默认参数
判断是否后台运行
初始化服务,
会对SIGHUP和SIGPIPE进行忽略设置
对于其他信号会由setupSignalHandlers
函数对其进行捕获,比如SIGINT信号捕获后由redis进行收尾工作,避免进程被暴力kill
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
根据createSharedObjects
函数创建共享对象,对于常用的对象,采用引用计数的方式进行复用
根据系统设置调整打开的文件数adjustOpenFilesLimit()
网络模型初始化,redis支持Unix和TCP两种模型,当客户端和服务端都在本机时,Unix通信更快(不需要解析协议头)
LRU过期策略初始化evictionPoolAlloc(); /* Initialize the LRU keys pool. */
初始化rdb和aof信息
初始化状态信息resetServerStats()
注册事件
慢日志初始化slowlogInit
后台线程创建
加载外部模块moduleLoadFromQueue
加载磁盘数据(优先加载AOF)loadDataFromDisk
进入事件主循环
命令传播
所谓命令传播,就是在复制的过程中,Redis是继续对外服务的,这将导致主从数据存在差异,此时会进行命令传播,需要一个命令传播机制,传播的时候会通过propagate()
函数调用 replicationFeedSlaves()
,在复制过程中,会将数据拷贝一份到复制积压缓冲区和从服务器 输出缓冲区中。
复制积压缓冲区作用:
在复制期间,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。
积压缓冲区是一个由主服务器维护的固定长度、先进先出的队列,默认大小为1M。如果超过了1M,那么会进行覆盖操作。而且对于复制积压缓冲区整个主节点只有一个,所有的从节点共享此缓冲区。
优先部分复制的场景:
slaveof no one
void replicaofCommand(client *c) {
/* The special host/port combination "NO" "ONE" turns the instance
* into a master. Otherwise the new master address is set. */
if (!strcasecmp(c->argv[1]->ptr,"no") &&
!strcasecmp(c->argv[2]->ptr,"one")) {
if (server.masterhost) {
replicationUnsetMaster();
sds client = catClientInfoString(sdsempty(),c);
serverLog(LL_NOTICE,"MASTER MODE enabled (user request from '%s')",
client);
sdsfree(client);
}
} else {
...
}
addReply(c,shared.ok);
}
/* 取消主从复制,将他自己设置为主,恢复最初状态. */
void replicationUnsetMaster(void) {
if (server.masterhost == NULL) return; /* Nothing to do. */
sdsfree(server.masterhost);
server.masterhost = NULL;
//
shiftReplicationId();
if (server.master) freeClient(server.master);
replicationDiscardCachedMaster();
cancelReplicationHandshake();
disconnectSlaves();
server.repl_state = REPL_STATE_NONE;
server.slaveseldb = -1;
server.repl_no_slaves_since = server.unixtime;
}
slaveof IP PORT
/* 将主节点的IP和PORT记录在RedisServer数据结构中 */
struct redisServer
{
...
char *masterhost; /* Hostname of master */
int masterport; /* Port of master */
...
}
/* 将主节点信息进行缓存 */
/* Set replication to the specified master address and port. */
void replicationSetMaster(char *ip, int port) {
int was_master = server.masterhost == NULL;
//记录新的主节点
sdsfree(server.masterhost);
server.masterhost = sdsnew(ip);
server.masterport = port;
if (server.master) {
freeClient(server.master);
}
disconnectAllBlockedClients(); /* Clients blocked in master, now slave. */
/* Force our slaves to resync with us as well. They may hopefully be able
* to partially resync with us, but we can notify the replid change. */
disconnectSlaves();
cancelReplicationHandshake();
//这里缓存主节点
if (was_master) replicationCacheMasterUsingMyself();
server.repl_state = REPL_STATE_CONNECT;
}
全量复制
有盘复制,当有多个slave需要同步的时候,会尽量对rdb进行复用
/* SYNC and PSYNC command implemenation. */
void syncCommand(client *c) {
....
/* Setup the slave as one waiting for BGSAVE to start. The following code
* paths will change the state if we handle the slave differently. */
c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
if (server.repl_disable_tcp_nodelay)
anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */
c->repldbfd = -1;
c->flags |= CLIENT_SLAVE;
listAddNodeTail(server.slaves,c);
/* Create the replication backlog if needed. */
if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) {
/* When we create the backlog from scratch, we always use a new
* replication ID and clear the ID2, since there is no valid
* past history. */
changeReplicationId();
clearReplicationId2();
createReplicationBacklog();
}
/* CASE 1: BGSAVE is in progress, with disk target. */
if (server.rdb_child_pid != -1 &&
server.rdb_child_type == RDB_CHILD_TYPE_DISK)
{
/* Ok a background save is in progress. Let's check if it is a good
* one for replication, i.e. if there is another slave that is
* registering differences since the server forked to save. */
client *slave;
listNode *ln;
listIter li;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
slave = ln->value;
if (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END) break;
}
/* To attach this slave, we check that it has at least all the
* capabilities of the slave that triggered the current BGSAVE. */
if (ln && ((c->slave_capa & slave->slave_capa) == slave->slave_capa)) {
/* Perfect, the server is already registering differences for
* another slave. Set the right state, and copy the buffer. */
copyClientOutputBuffer(c,slave); /*拷贝输出缓存区内容*/
replicationSetupSlaveForFullResync(c,slave->psync_initial_offset);
serverLog(LL_NOTICE,"Waiting for end of BGSAVE for SYNC");
} else {
/* No way, we need to wait for the next BGSAVE in order to
* register differences. */
serverLog(LL_NOTICE,"Can't attach the replica to the current BGSAVE. Waiting for next BGSAVE for SYNC");
}
。。。
}
无盘复制,直接fork放在内存
/* SYNC and PSYNC command implemenation. */
void syncCommand(client *c) {
...
/* CASE 2: BGSAVE is in progress, with socket target. */
} else if (server.rdb_child_pid != -1 &&
server.rdb_child_type == RDB_CHILD_TYPE_SOCKET)
{
/* There is an RDB child process but it is writing directly to
* children sockets. We need to wait for the next BGSAVE
* in order to synchronize. */
serverLog(LL_NOTICE,"Current BGSAVE has socket target. Waiting for next BGSAVE for SYNC");
/* CASE 3: There is no BGSAVE is progress. */
} else {
if (server.repl_diskless_sync && (c->slave_capa & SLAVE_CAPA_EOF)) {
/* Diskless replication RDB child is created inside
* replicationCron() since we want to delay its start a
* few seconds to wait for more slaves to arrive. */
if (server.repl_diskless_sync_delay)
serverLog(LL_NOTICE,"Delay next BGSAVE for diskless SYNC");
} else {
//这里
if (server.aof_child_pid == -1) {
startBgsaveForReplication(c->slave_capa);
} else {
}
}
}
return;
}
增量复制,主要看从节点的复制偏移量是否位于主节点复制积压缓冲区,从节点的主节点是否发生变化等。
int masterTryPartialResynchronization(client *c) {
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
/* Run id "?" is used by slaves that want to force a full resync. */
if (master_replid[0] != '?') {
if (strcasecmp(master_replid, server.replid) &&
strcasecmp(master_replid, server.replid2))
{
} else {
}
} else {
serverLog(LL_NOTICE,"Full resync requested by replica %s",
replicationGetSlaveName(c));
}
goto need_full_resync;
}
/* We still have the data our slave is asking for? */
if (!server.repl_backlog ||
psync_offset < server.repl_backlog_off ||
psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
{
if (psync_offset > server.master_repl_offset) {
}
goto need_full_resync;
}
need_full_resync:
/* We need a full resync for some reason... Note that we can't
* reply to PSYNC right now if a full SYNC is needed. The reply
* must include the master offset at the time the RDB file we transfer
* is generated, so we need to delay the reply to that moment. */
return C_ERR;
}
从节点接收数据:
if(aof_is_enabled) stopAppendOnly();//暂停aof持久化
signalFlushedDb(-1);
emptyDb(
-1,
server.repl_slave_lazy_flush ? EMPTYDB_ASYNC : EMPTYDB_NO_FLAGS,
replicationEmptyDbCallback);//清空数据库
aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE);//删除可读事件
serverLog(LL_NOTICE, "MASTER <-> REPLICA sync: Loading DB in memory");
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
if (rdbLoad(server.rdb_filename,&rsi) != C_OK) { //加载RDB
serverLog(LL_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
cancelReplicationHandshake();
//暂停
if (aof_is_enabled) restartAOF();
return;
}
/* Final setup of the connected slave <- master link */
zfree(server.repl_transfer_tmpfile);
close(server.repl_transfer_fd);
replicationCreateMasterClient(server.repl_transfer_s,rsi.repl_stream_db);
server.repl_state = REPL_STATE_CONNECTED;
server.repl_down_since = 0;
/* 记录主节点的runid和偏移量 */
memcpy(server.replid,server.master->replid,sizeof(server.replid));
server.master_repl_offset = server.master->reploff;
clearReplicationId2();
if (server.repl_backlog == NULL) createReplicationBacklog();
if (aof_is_enabled) restartAOF();//重启aof
两个定时检测任务
主从复制对过期键的处理:
主:只会载入未过期的
从:全部载入,后期主从会同步
哨兵启动模式:因为哨兵和redisServer共用了部分代码,所以通过设置参数有两种启动方式
redis-sentinel sentinel.conf
redis-server sentinel.conf –-sentinel
当redis服务以哨兵模式启动后,会创建一个struct sentinelState
的全局实例sentinel,sentinel启动后会调用 createSentinelRedisInstance
创建sentinelRedisInstance
实例用于处理与master的连接,实例创建好并不代表连接创建完成,只是准备好了连接的结构,后续会在定时任务中进行创建连接,建立连接会创建两条连接,分别是link->cc,link->pc,cc用来发送ping和info信息,pc用来向master注册hello频道并定时发送消息。
其中,监控不同master之间的相同哨兵,会对连接进行共用,以减少频繁的连接建立,比如5个哨兵监控一百个master,只会建立5条连接,而不是500条。
哨兵的状态数据结构
/* Main state. */
struct sentinelState {
char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
//记录的当前纪元用于故障转移用
uint64_t current_epoch; /* Current epoch. */
//记录了所有被该哨兵监控的主服务器。
//其中key为主服务器名称,value为sentinelRedisInstances对象
dict *masters; /* Dictionary of master sentinelRedisInstances.
Key is the instance name, value is the
sentinelRedisInstance structure pointer. */
int tilt; /* Are we in TILT mode? */
int running_scripts; /* Number of scripts in execution right now. */
mstime_t tilt_start_time; /* When TITL started. */
mstime_t previous_time; /* Last time we ran the time handler. */
list *scripts_queue; /* Queue of user scripts to execute. */
char *announce_ip; /* IP addr that is gossiped to other sentinels if
not NULL. */
int announce_port; /* Port that is gossiped to other sentinels if
non zero. */
unsigned long simfailure_flags; /* Failures simulation. */
int deny_scripts_reconfig; /* Allow SENTINEL SET ... to change script
paths at runtime? */
} sentinel;
sentinelRedisInstance哨兵实例
typedef struct sentinelRedisInstance {
int flags; /* See SRI_... defines 用来表示当前实例的类型和状态 */
char *name; /*哨兵记录的master名字,从服务器和哨兵的话是用的是IP+Port */
char *runid; /* Run ID of this instance, or unique ID if is a Sentinel.*/
uint64_t config_epoch; /* 用于实现故障转移. */
sentinelAddr *addr; /* 实例地址 */
instanceLink *link; /* Link to the instance, may be shared for Sentinels. */
mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */
mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
we received a hello from this Sentinel
via Pub/Sub. */
mstime_t last_master_down_reply_time; /* Time of last reply to
SENTINEL is-master-down command. */
mstime_t s_down_since_time; /*主观宕机时间 Subjectively down since time. */
mstime_t o_down_since_time; /* 可观宕机时间Objectively down since time. */
mstime_t down_after_period; /*实例无响应多久被认为是主观宕机。 Consider it down after that period. */
mstime_t info_refresh; /* Time at which we received INFO output from it. */
dict *renamed_commands; /* Commands renamed in this instance:
Sentinel will use the alternative commands
mapped on this table to send things like
SLAVEOF, CONFING, INFO, ... */
/* Role and the first time we observed it.
* This is useful in order to delay replacing what the instance reports
* with our own configuration. We need to always wait some time in order
* to give a chance to the leader to report the new configuration before
* we do silly things. */
int role_reported;
mstime_t role_reported_time;
mstime_t slave_conf_change_time; /* Last time slave master addr changed. */
/* Master specific. */
dict *sentinels; /* 用于记录监控相同主节点其他哨兵实例。该字典以哨兵名字为key,以哨兵实例sentinelRedisInstance结构为key;Other sentinels monitoring the same master. */
dict *slaves; /* 用于记录该主节点实例的所有从节点实例。该字典以从节点名字为key,以从节点实例sentinelRedisInstance结构为key; Slaves for this master instance. */
unsigned int quorum;/* 多少哨兵同意下线,才表示可观宕机Number of sentinels that need to agree on failure. */
int parallel_syncs; /* How many slaves to reconfigure at same time. */
char *auth_pass; /* Password to use for AUTH against master & slaves. */
/* Slave specific. */
mstime_t master_link_down_time; /*主从复制超时时间 Slave replication link down time. */
int slave_priority; /* 从节点级别 Slave priority according to its INFO output. */
mstime_t slave_reconf_sent_time; /* 从节点配置Time at which we sent SLAVE OF */
struct sentinelRedisInstance *master; /*所属的master Master instance if it's slave. */
char *slave_master_host; /* Master host as reported by INFO */
int slave_master_port; /* Master port as reported by INFO */
int slave_master_link_status; /* Master link status as reported by INFO */
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
char *leader; /* If this is a master instance, this is the runid of
the Sentinel that should perform the failover. If
this is a Sentinel, this is the runid of the Sentinel
that this Sentinel voted as leader. */
uint64_t leader_epoch; /* leader的epoch Epoch of the 'leader' field. */
uint64_t failover_epoch; /* Epoch of the currently started failover. */
int failover_state; /*故障转移时,状态参数 See SENTINEL_FAILOVER_STATE_* defines. */
mstime_t failover_state_change_time;
mstime_t failover_start_time; /* Last failover attempt start time. */
mstime_t failover_timeout; /* 故障转移超时时间 Max time to refresh failover state. */
mstime_t failover_delay_logged; /* For what failover_start_time value we
logged the failover delay. */
struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
/* Scripts executed to notify admin or reconfigure clients: when they
* are set to NULL no script is executed. */
char *notification_script;
char *client_reconfig_script;
sds info; /* cached INFO output */
} sentinelRedisInstance;
三类命令
info
每隔十秒,每个哨兵会向master和slave发送info命令获取最新的拓扑结构,当有新的slave加入后会及时感知到,当某个slave的主节点状态变成客观宕机之后,周期会由10s变成1s,因为他随时会变成master
/* Send INFO to masters and slaves, not sentinels. */
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
retval = redisAsyncCommand(ri->link->cc,
sentinelInfoReplyCallback, ri, "%s",
sentinelInstanceMapCommand(ri,"INFO"));
if (retval == C_OK) ri->link->pending_commands++;
}
针对返回信息哨兵会进行分析,可以获取主从节点的连接状态角色,runid,优先级等信息
当从节点转变成为主节点时,会进行故障转移,此时info信息每秒发送一次
/* If this is a slave of a master in O_DOWN condition we start sending
* it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
* period. In this state we want to closely monitor slaves in case they
* are turned into masters by another Sentinel, or by the sysadmin.
*
* Similarly we monitor the INFO output more often if the slave reports
* to be disconnected from the master, so that we can have a fresh
* disconnection time figure.
* 如果该从节点的主节点可观投票down机了,那么该slave由10s变成1s进行info,因为他随时都有可能变成主节点
* */
if ((ri->flags & SRI_SLAVE) &&
((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
(ri->master_link_down_time != 0)))
{
info_period = 1000;
} else {
info_period = SENTINEL_INFO_PERIOD;
}
PING
/* Send PING to all the three kinds of instances. */
if ((now - ri->link->last_pong_time) > ping_period &&
(now - ri->link->last_ping_time) > ping_period/2) {
sentinelSendPing(ri);
}
hello
默认情况下,哨兵会以每2秒一次的速度向主和从服务器发送消息
#define SENTINEL_PUBLISH_PERIOD 2000
/* PUBLISH hello messages to all the three kinds of instances. */
if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
sentinelSendHello(ri);
}
作用为发现新的哨兵节点,每个哨兵会去监听自己监控的每个主从节点对应的hello频道,然后感知到同样在监听这个节点的其他哨兵存在,同时每个哨兵还会和其他哨兵交换对节点的监控配置,并互相进行同步
总体运行流程:
启动初始化
定时任务,哨兵模式下,sentinel为了自动管理redis服务器,在serverCron中执行sentinelTimer函数,100ms执行一次
主观下线判断
客观下线判断
is-master-down-by-addr
命令进行查询客观下线判定完成后会开始选举领头的哨兵,来进行新主节点的切换
从节点成为新主节点的候选条件如下:
针对有多个从节点时,优先级为:
int compareSlavesForPromotion(const void *a, const void *b) {
sentinelRedisInstance **sa = (sentinelRedisInstance **)a,
**sb = (sentinelRedisInstance **)b;
char *sa_runid, *sb_runid;
if ((*sa)->slave_priority != (*sb)->slave_priority)
return (*sa)->slave_priority - (*sb)->slave_priority;
/* If priority is the same, select the slave with greater replication
* offset (processed more data from the master). */
if ((*sa)->slave_repl_offset > (*sb)->slave_repl_offset) {
return -1; /* a < b */
} else if ((*sa)->slave_repl_offset < (*sb)->slave_repl_offset) {
return 1; /* a > b */
}
/* If the replication offset is the same select the slave with that has
* the lexicographically smaller runid. Note that we try to handle runid
* == NULL as there are old Redis versions that don't publish runid in
* INFO. A NULL runid is considered bigger than any other runid. */
sa_runid = (*sa)->runid;
sb_runid = (*sb)->runid;
if (sa_runid == NULL && sb_runid == NULL) return 0;
else if (sa_runid == NULL) return 1; /* a > b */
else if (sb_runid == NULL) return -1; /* a < b */
return strcasecmp(sa_runid, sb_runid);
}
选中哨兵会向被选中的从节点发送slaveof no one ,然后开始发送info命令,来看他是否成为了主节点
void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
int retval;
/* We can't send the command to the promoted slave if it is now
* disconnected. Retry again and again with this state until the timeout
* is reached, then abort the failover. */
if (ri->promoted_slave->link->disconnected) {
if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
sentinelAbortFailover(ri);
}
return;
}
/* Send SLAVEOF NO ONE command to turn the slave into a master.
* We actually register a generic callback for this command as we don't
* really care about the reply. We check if it worked indirectly observing
* if INFO returns a different role (master instead of slave). */
retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
if (retval != C_OK) return;
sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion",
ri->promoted_slave,"%@");
ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
ri->failover_state_change_time = mstime();
}
新的master上线后,会将原来从节点维持的主从关系进行清除,然后指向新的master