在的redis启动函数main(server.c文件)中,对哨兵模式进行了检查,如果是哨兵模式,将调用initSentinelConfig和initSentinel进行初始化,initServer函数中会注册哨兵的时间事件,最后调用sentinelIsRunning运行哨兵实例,

int main(int argc, char **argv) {
// 省略... // 检查哨兵模式
server.sentinel_mode = checkForSentinelMode(argc,argv);
initServerConfig();
// 省略... if (server.sentinel_mode) {
initSentinelConfig(); // 初始化哨兵配置
initSentinel(); // 初始化哨兵
} // 省略...
// 初始化服务
initServer();
// 省略...
if (!server.sentinel_mode) { // 非哨兵模式
// 省略...
} else {
ACLLoadUsersAtStartup();
InitServerLast();
// 运行哨兵实例
sentinelIsRunning();
if (server.supervised_mode == SUPERVISED_SYSTEMD) {
redisCommunicateSystemd("STATUS=Ready to accept connections\n");
redisCommunicateSystemd("READY=1\n");
}
}
// 省略... aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}

哨兵初始化

哨兵模式校验

checkForSentinelMode

checkForSentinelMode函数在server.c文件中,用于校验是否是哨兵模式,可以看到有两种方式校验哨兵模式:

  1. 直接执行redis-sentinel 命令
  2. 执行redis-server命令,命令参数中带有–sentinel参数
int checkForSentinelMode(int argc, char **argv) {
int j;
// 直接执行redis-sentinel
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1; // 执行的命令参数中,是否有–sentinel
return 0;
}

初始化配置项

initSentinelConfig

initSentinelConfig函数在sentinel.c文件中,用于初始化哨兵配置:

  1. 将哨兵实例的端口号设置为REDIS_SENTINEL_PORT,默认值26379
  2. protected_mode设置为0,表示允许外部链接哨兵实例,而不是只能通过127.0.0.1本地连接 server
#define REDIS_SENTINEL_PORT 26379

void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT; /* 设置默认端口 */
server.protected_mode = 0; /* 允许外部链接哨兵实例 */
}

initSentinel

在看initSentinel函数之前,首先看下Redis中哨兵sentinel对象对应的结构体sentinelState:

  • current_epoch:当前纪元,投票选举Leader时使用,纪元可以理解为投票的轮次
  • masters:监控的master节点哈希表,Key为master节点名称, value为master节点对应sentinelRedisInstance实例的指针
struct sentinelState {
char myid[CONFIG_RUN_ID_SIZE+1]; /* sentinel ID */
uint64_t current_epoch; /* 当前的纪元(投票轮次)*/
dict *masters; /* 监控的master节点哈希表,Key为master节点名称, value为master节点对应的实例对象的指针 */
int tilt; /* TILT模式 */
int running_scripts;
mstime_t tilt_start_time;
mstime_t previous_time;
list *scripts_queue;
char *announce_ip;
int announce_port;
unsigned long simfailure_flags;
int deny_scripts_reconfig;
char *sentinel_auth_pass;
char *sentinel_auth_user;
int resolve_hostnames;
int announce_hostnames;
} sentinel;

sentinelRedisInstance是一个通用的结构体,在sentinel.c文件中定义,它既可以表示主节点,也可以表示从节点或者其他哨兵实例,从中选选出了一些主要的内容:

typedef struct  {
int flags; /* 一些状态标识 */
char *name; /* Master name from the point of view of this sentinel. */
char *runid; /* 实例的运行ID */
uint64_t config_epoch; /* 配置的纪元. */
mstime_t s_down_since_time; /* 主观下线时长 */
mstime_t o_down_since_time; /* 客观下线时长 */
dict *sentinels; /* 监控同一主节点的其他哨兵实例 */
dict *slaves; /* slave节点(从节点) */
/* 故障切换 */
char *leader; /* 如果是master节点,保存了需要执行故障切换的哨兵leader的runid,如果是一个哨兵,保存的是这个哨兵投票选举的leader*/
uint64_t leader_epoch; /* leader纪元 */
uint64_t failover_epoch;
int failover_state; /* 故障切换状态 */ // 省略...
} sentinelRedisInstance;

initSentinel函数同样在sentinel.c文件中,用于初始化哨兵,由于哨兵实例与普通Redis实例不一样,所以需要替换Redis中的命令,添加哨兵实例命令,哨兵实例使用的命令在sentinelcmds中定义:

  1. 将server.commands和server.orig_commands保存的常规Redis命令清除
  2. 遍历sentinelcmds哨兵实例专用命令,将命令添加到server.commands和server.orig_commands中
  3. 初始化sentinel实例中的数据项
// 哨兵实例下的命令
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"fast @connection",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"admin",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"pub-sub fast",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"random @dangerous",0,NULL,0,0,0,0,0},
{"role",sentinelRoleCommand,1,"fast read-only @dangerous",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"admin random @connection",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"admin",0,NULL,0,0,0,0,0},
{"auth",authCommand,-2,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"hello",helloCommand,-1,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"acl",aclCommand,-2,"admin",0,NULL,0,0,0,0,0,0},
{"command",commandCommand,-1, "random @connection", 0,NULL,0,0,0,0,0,0}
}; /* 初始化哨兵 */
void initSentinel(void) {
unsigned int j; /* 将常规的Redis命令移除,增加哨兵实例专用的命令 */
dictEmpty(server.commands,NULL);
dictEmpty(server.orig_commands,NULL);
ACLClearCommandID();
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j;
cmd->id = ACLGetCommandID(cmd->name);
// 添加到server.commands
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
// 添加到server.orig_commands
retval = dictAdd(server.orig_commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
if (populateCommandTableParseFlags(cmd,cmd->sflags) == C_ERR)
serverPanic("Unsupported command flag");
} /* 初始化其他数据项 */
// current_epoch初始化为0
sentinel.current_epoch = 0;
// 监控的master节点实例对象
sentinel.masters = dictCreate(&instancesDictType,NULL);
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG;
sentinel.sentinel_auth_pass = NULL;
sentinel.sentinel_auth_user = NULL;
sentinel.resolve_hostnames = SENTINEL_DEFAULT_RESOLVE_HOSTNAMES;
sentinel.announce_hostnames = SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES;
memset(sentinel.myid,0,sizeof(sentinel.myid));
server.sentinel_config = NULL;
}

启动哨兵实例

sentinelIsRunning

sentinelIsRunning函数在sentinel.c文件中,用于启动哨兵实例:

  1. 校验是否设置了哨兵实例的ID,如果未设置,将随机生成一个ID
  2. 调用sentinelGenerateInitialMonitorEvents向监控的主节点发送+monitor事件
void sentinelIsRunning(void) {
int j; /* 校验myid是否为0 */
for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
if (sentinel.myid[j] != 0) break; if (j == CONFIG_RUN_ID_SIZE) {
/* 随机生成ID */
getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
sentinelFlushConfig();
} serverLog(LL_WARNING,"Sentinel ID is %s", sentinel.myid); /* 向监控的主节点发送+monitor事件 */
sentinelGenerateInitialMonitorEvents();
} /* 向监控的主节点发布事件 */
void sentinelGenerateInitialMonitorEvents(void) {
dictIterator *di;
dictEntry *de;
// 获取监控的主节点
di = dictGetIterator(sentinel.masters);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 向主节点发送监控事件
sentinelEvent(LL_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
}
dictReleaseIterator(di);
}

哨兵时间事件

在initServer函数中,调用aeCreateTimeEvent注册了时间事件,周期性的执行serverCron函数,serverCron函数中通过server.sentinel_mode判断是否是哨兵模式,如果是哨兵模式,调用sentinelTimer执行哨兵事件:

void initServer(void) {

    // 省略...
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
// 省略...
} int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 省略... // 如果是哨兵模式,调用sentinelTimer执行哨兵事件
if (server.sentinel_mode) sentinelTimer(); // 省略...
}

sentinelTimer

sentinelTimer在sentinel.c文件中,sentinelTimer函数会周期性的执行:

void sentinelTimer(void) {
sentinelCheckTiltCondition();
// 处理RedisInstances,传入的参数是当前哨兵实例维护的主节点的哈希表,里面记录当前节点监听的主节点
sentinelHandleDictOfRedisInstances(sentinel.masters);
sentinelRunPendingScripts();
sentinelCollectTerminatedScripts();
sentinelKillTimedoutScripts();
// 调整sentinelTimer的执行频率
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

sentinelHandleDictOfRedisInstances

sentinelHandleDictOfRedisInstances函数中会对传入的当前哨兵实例监听的主节点哈希表进行遍历:

  1. 获取哈希表中的每一个节点,节点类型是sentinelRedisInstance
  2. 调用sentinelHandleRedisInstance检测哨兵监听节点的状态
  3. 如果sentinelHandleRedisInstance是主节点,由于主节点里面保存了监听该主节点的其他哨兵实例以及从节点,所以递归调用sentinelHandleDictOfRedisInstances对其他的节点进行检测
/* sentinelHandleDictOfRedisInstances */
void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
di = dictGetIterator(instances);
// 遍历所有的sentinelRedisInstance实例
while((de = dictNext(di)) != NULL) {
// 获取每一个sentinelRedisInstance
sentinelRedisInstance *ri = dictGetVal(de);
// 调用sentinelHandleRedisInstance检测哨兵监听节点的状态
sentinelHandleRedisInstance(ri);
// 如果是sentinelRedisInstance是主节点,主节点里面保存了监听该主节点的其他哨兵实例以及从节点
if (ri->flags & SRI_MASTER) {
// 递归调用,处理从节点
sentinelHandleDictOfRedisInstances(ri->slaves);
// 递归调用,处理其他哨兵实例
sentinelHandleDictOfRedisInstances(ri->sentinels);
if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
switch_to_promoted = ri;
}
}
}
if (switch_to_promoted)
sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
dictReleaseIterator(di);
}

检测哨兵监听的节点状态

sentinelHandleRedisInstance

sentinelHandleRedisInstance函数对传入的sentinelRedisInstance实例,进行状态检测,主要处理逻辑如下:

  1. 调用sentinelReconnectInstance对实例的连接状态进行判断,如果**连接中断尝试重新与实例建立连接 **
  2. 调用sentinelSendPeriodicCommands向实例发送PING INFO等命令
  3. sentinelCheckSubjectivelyDown判断实例是否主观下线
  4. 如果实例是master节点,调用sentinelCheckObjectivelyDown判断是否客观下线、是否需要执行故障切换

/* Perform scheduled operations for the specified Redis instance. */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { // 如果监听的节点连接中断,尝试重新建立连接
sentinelReconnectInstance(ri);
// 发送PING INFO等命令
sentinelSendPeriodicCommands(ri); // 检查是否是TILT模式
if (sentinel.tilt) {
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
} // 判断主观下线
sentinelCheckSubjectivelyDown(ri); /* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
} // 如果是master节点
if (ri->flags & SRI_MASTER) {
// 判断客观下线
sentinelCheckObjectivelyDown(ri);
// 是否需要启动故障切换
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障切换
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对master节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

重新连接

sentinelReconnectInstance

sentinelReconnectInstance函数用于检测实例的连接状态,如果中断进行重连,主要处理逻辑如下:

  1. 检测连接是否中断,如果未中断直接返回

  2. 检查端口是否为0,0被认为是不合法的端口

  3. 从sentinelRedisInstance实例中获取instanceLink,instanceLink的定义在sentinel.c文件中,里面记录了哨兵和主节点的两个连接,分别为用于发送命令的连接cc和用于发送Pub/Sub消息的连接pc

    typedef struct instanceLink {
    int refcount;
    int disconnected;
    int pending_commands;
    redisAsyncContext *cc; /* 用于发送命令的连接 */
    redisAsyncContext *pc; /* 用于发送Pub/Sub消息的连接 */
    mstime_t cc_conn_time; /* cc 连接时间 */
    mstime_t pc_conn_time; /* pc 连接时间 */
    mstime_t pc_last_activity;
    mstime_t last_avail_time; /* 上一次收到实例回复PING命令(需要被认定为合法)的时间 */
    mstime_t act_ping_time; /* 当收到PONG消息的时候会设置为0,在下次发送PING命令时设置为当前时间 */
    mstime_t last_ping_time; /* 上次发送PING命令时间,在出现故障时可以通过判断发送时间避免多次发送PING命令 */
    mstime_t last_pong_time; /* 上次收到PONG消息的时间 */
    mstime_t last_reconn_time; /* 上次执行重连的时间 */
    } instanceLink;
  4. 校验距离上次重连时间是否小于PING的检测周期SENTINEL_PING_PERIOD,如果小于说明距离上次重连时间过近,直接返回即可

    SENTINEL_PING_PERIOD在server.c中定义,默认1000毫秒

    #define SENTINEL_PING_PERIOD 1000
  5. 对用于发送命令的连接判断,如果连接为NULL,调用redisAsyncConnectBind函数进行重连

  6. 对用于处理发送 Pub/Sub 消息的连接进行判断,如果连接为NULL,调用redisAsyncConnectBind函数进行重连

void sentinelReconnectInstance(sentinelRedisInstance *ri) {
// 检查连接是否中断
if (ri->link->disconnected == 0) return;
if (ri->addr->port == 0) return; /* 检查端口是否为0,0被认为是不合法的端口 */
// 获取instanceLink
instanceLink *link = ri->link;
mstime_t now = mstime();
// 校验距离上次重连时间是否小于哨兵PING的周期设置
if (now - ri->link->last_reconn_time < SENTINEL_PING_PERIOD) return;
ri->link->last_reconn_time = now; /* 处理用于发送命令的连接 */
if (link->cc == NULL) {
// 进行连接
link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
if (link->cc && !link->cc->err) anetCloexec(link->cc->c.fd);
// 省略...
}
/* 处理用于发送 Pub/Sub 消息的连接 */
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
if (link->pc && !link->pc->err) anetCloexec(link->pc->c.fd);
// 省略...
}
if (link->cc && (ri->flags & SRI_SENTINEL || link->pc))
link->disconnected = 0;
}

发送命令

sentinelSendPeriodicCommands

sentinelSendPeriodicCommands用于向实例发送命令:

void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
mstime_t now = mstime();
mstime_t info_period, ping_period;
int retval; // 省略... /* 向主节点和从节点发送INFO命令 */
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
// 发送INFO命令
retval = redisAsyncCommand(ri->link->cc,
sentinelInfoReplyCallback, ri, "%s",
sentinelInstanceMapCommand(ri,"INFO"));
if (retval == C_OK) ri->link->pending_commands++;
} if ((now - ri->link->last_pong_time) > ping_period &&
(now - ri->link->last_ping_time) > ping_period/2) {
// 向实例发送PING命令
sentinelSendPing(ri);
} /* PUBLISH hello messages to all the three kinds of instances. */
if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
// 发送PUBLISH命令
sentinelSendHello(ri);
}
}

主观下线

sentinelCheckSubjectivelyDown

sentinelCheckSubjectivelyDown函数用于判断是否主观下线。

标记主观下线的两个条件

  • 距离上次发送PING命令的时长超过了down_after_period的值,down_after_period的值在sentinel.conf 配置文件中配置,对应的配置项为down-after-milliseconds ,默认值30s
  • 哨兵认为实例是主节点(ri->flags & SRI_MASTE),但是实例向哨兵返回的角色是从节点(ri->role_reported == SRI_SLAVE) 并且当前时间-实例报告消息的时间role_reported_time大于down_after_period加上SENTINEL_INFO_PERIOD乘以2的时间 ,SENTINEL_INFO_PERIOD 是发送INFO命令的时间间隔,也就是说实例上次成功向哨兵报告角色的时间,已经超过了限定时间(down_after_period加上SENTINEL_INFO_PERIOD*2)

满足以上两个条件之一哨兵将会把sentinelRedisInstance判断为主观下线,flag标记会添加SRI_S_DOWN状态。

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
mstime_t elapsed = 0;
// 如果act_ping_time不为0
if (ri->link->act_ping_time)
elapsed = mstime() - ri->link->act_ping_time; // 计算距离上次发送PING命令的间隔时间
else if (ri->link->disconnected) // 如果连接断开
elapsed = mstime() - ri->link->last_avail_time; // 计算距离最近一次收到PING命令回复的间隔时间 if (ri->link->cc &&
(mstime() - ri->link->cc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
ri->link->act_ping_time != 0 &&
(mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
(mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
{
instanceLinkCloseConnection(ri->link,ri->link->cc);
} if (ri->link->pc &&
(mstime() - ri->link->pc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
(mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
{
instanceLinkCloseConnection(ri->link,ri->link->pc);
} /*
* 标记主观下线的两个条件(或的关系)
* 1) 距离上次发送PING命令的时长超过了down_after_period
* 2) 哨兵认为实例是主节点(ri->flags & SRI_MASTE),但是实例向哨兵返回的角色是从节点(ri->role_reported == SRI_SLAVE) 并且当前时间-实例返回消息的时间大于down_after_period加上SENTINEL_INFO_PERIOD*2的时间 */
if (elapsed > ri->down_after_period ||
(ri->flags & SRI_MASTER &&
ri->role_reported == SRI_SLAVE &&
mstime() - ri->role_reported_time >
(ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
{
/* 主观下线 */
if ((ri->flags & SRI_S_DOWN) == 0) {
// 发送+sdown事件
sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN; // 更改状态
}
} else {
/* Is subjectively up */
if (ri->flags & SRI_S_DOWN) {
sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
}
}
}

客观下线

如果是主节点,将会调用sentinelCheckObjectivelyDown函数判断客观下线,之后调用sentinelStartFailoverIfNeeded判断是否需要执行故障切换。

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】哨兵初始化和主观下线的更多相关文章

  1. Redis 哨兵

    作用 Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入. 主要提供了配置提供者,通知,哨兵的监控和自动故障转移功能.哨兵的核心功能是主节点的自动故障转移. 下面是Red ...

  2. 【转】Redis哨兵(Sentinel)模式

    主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式. 一.哨兵 ...

  3. Redis哨兵(Sentinel)模式

    Redis哨兵(Sentinel)模式   主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式 ...

  4. 【集群】Redis哨兵(Sentinel)模式

    主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式. 一.哨兵 ...

  5. Redis哨兵的配置和原理

    哨兵 在一个典型的一主多从的Redis系统中,当主数据库遇到异常中断服务后,需要手动选择一个从数据库升级为主数据库,整个过程需要人工介入,难以自动化. Redis2.8提供了哨兵2.0(2.6提供了1 ...

  6. Redis源码解析:21sentinel(二)定期发送消息、检测主观下线

    六:定时发送消息 哨兵每隔一段时间,会向其所监控的所有实例发送一些命令,用于获取这些实例的状态.这些命令包括:"PING"."INFO"和"PUBLI ...

  7. redis哨兵主从自动切换

    1.用的是TP5框架,改写框架自带的redis类 thinkphp/library/think/cache/driver/Redis.php //两台服务器都配置好了监控哨兵 //主从配置要设置好密码 ...

  8. Redis哨兵(Sentinel)模式快速入门

    更多内容,欢迎关注微信公众号:全菜工程师小辉.公众号回复关键词,领取免费学习资料. 当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用. ...

  9. Redis 哨兵模式(Sentinel)

    上一篇我们介绍了 redis 主从节点之间的数据同步复制技术,通过一次全量复制和不间断的命令传播,可以达到主从节点数据同步备份的效果,一旦主节点宕机,我们可以选择一个工作正常的 slave 成为新的主 ...

随机推荐

  1. mpvue下拉刷新

    1 开启下拉刷新 在想要下拉刷新的页面的 main.json 里,添加: { "navigationBarTitleText": "页面标题", "e ...

  2. Ubu18.0-NVIDIA显卡驱动重装

    //图片仅供参考,请勿代入 问题情况:电脑装了双系统,WIN10+Ubu,Ubu分辨率不稳定,经常发生变化 显卡型号:打开设备管理器进行查看 解决方法:重装NVIDIA显卡驱动 1.去英伟达官网下载自 ...

  3. 在UnityUI中绘制线状统计图

    ##先来个效果图 觉得不好看可以自己调整 ##1.绘制数据点 线状图一般由数据点和连线组成 在绘制连线之前,我们先标出数据点 这里我选择用Image图片来绘制数据点 新建Canvas,添加空物体Gra ...

  4. 学习打卡——Mybatis—Plus

    今天看完了Mybatis-Plus的视频,在某些方面来看MP确实简化了很多操作,比如自动生成代码等等.学习过程的代码实例也到同步到了gitee和github

  5. 安装支付宝小程序的 mini-ali-ui

    1.打开文件的根目录 2.使用npm init -y初始化 3. npm install mini-ali-ui --save 安装 4.在json文件里面定义 例如使用loading加载中的组件 完 ...

  6. 3种方法改变this的指向

    <body>     <div style="width: 200px;height: 200px;hotpink;"></div>     & ...

  7. GitHub 自动合并 pr 的机器人——auto-merge-bot

    本文首发于 Nebula Graph Community 公众号 背景 作为一款开源的分布式图数据库产品,Nebula 所有的研发流程都在 GitHub 上运作.基于 GitHub 生态 Nebula ...

  8. 小程序开发之获取客户来源 scene 场景值 手机设备信息

    为什么要获取客户来源 用作数据分析,根据客户来源,做精准转化! 判断客户来源入口方式 1.通过官方的scene场景值 常见场景值 场景值ID 说明 1001 发现栏小程序主入口,「最近使用」列表 10 ...

  9. ArcGIS建筑物简化和建筑物群聚合算法实验

    一.下载OSM数据 首先从OpenStreetMap官网下载我们需要的实验数据,这里我选择清华和北大校园作为本次实验数据 二.数据处理 将我们下载的实验数据导入ArcGIS.由于OSM数据是.osm格 ...

  10. 《Mybatis 手撸专栏》第6章:数据源池化技术实现

    作者:小傅哥 博客:https://bugstack.cn - 手写Mybatis系列文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 码农,只会做不会说? 你有发现吗,其实很大一部分码农 ...