##Redis:使用SET命令实现简单的高可用(HA)
我们的需求:
假设有多个相同的进程实例分别在多个主机上运行,但是逻辑上同一时刻至多只能有一个进程实例在“工作”。
只有当在“工作”的那个进程实例由于某种异常原因宕掉,其余的进程实例进行争抢令牌。
争抢到令牌的那个进程实例,接替之前异常的那个进程实例,继续“工作”。
即,一个进程实例挂了,有别的进程实例能知道,并且及时接管异常进程实例的工作,保证业务流正常。
我们可设计一个令牌系统(Token)。
在系统任意时刻,最多只能有一个进程持有此令牌,持有令牌的进程实例的状态称为MASTER,其余的“候补”进程实例状态称为SLAVER。
系统最开始时,多个进程实例应当争抢此令牌,抢到令牌的MASTER实例,执行业务工作,其余的SLAVER进程实例,在不断地“虎视眈眈”地定时监控探测:那个MASTER家伙是否已经挂了?挂了的话SLAVER进程就会去争抢令牌。
主要涉及到一下几方面:
1.令牌保存在哪里?
2.进程实例如何“拿”(请求)到令牌?
3.进程实例如何知道令牌已经被别人“拿走”了?(状态)
4.谁来分配令牌的归属?
我们可以通过Redis的SET和GET等命令,简单地完成上面的工作。
SET key value EX second NX
将字符串值 value 关联到 key。
EX second :设置键的过期时间为 second 秒。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
返回值:
SET 在设置操作成功完成时,才返回 OK 。
由于参数中设置NX,假设因为NX条件没达到而造成设置操作未执行,则此命令将返回空批量回复(NULL Bulk Reply)。
注:完整的SET命令请移步:http://redisdoc.com/string/set.html。
到目前为止,有没有点“想法”?
实现逻辑:
假定有两个进程实例A以及B,有一个Redis服务实例S。
系统初始化时,A和B都尝试在S中执行SET命令,将一个VALUE与一个KEY进行关联。
其中KEY就是我们的令牌(Token)。
KEY对于A和B来说,是相互约定好的,值是相等的。
VALUE对于A和B来说,值是不相等的,是包含了自己本进程独一无二的标识信息。(例如身份证)
A和B对同一个KEY,都想将这个KEY与自己的独有信息标识关联起来。
A和B通过Redis API接口,将关联请求(SET命令)通过TCP经网络传输到达S。
S收到来自A和B的两个请求,将请求依次放入串行请求队列,先执行的请求将成功返回,后执行的请求将失败返回。
回想刚刚的问题:
1.令牌保存在Redis服务端;
2.进程实例通过API接口,请求获取到令牌;
3.第二次关联请求(SET)失败,返回的这个“失败”状态到进程实例,就知道这个令牌已经被占用了;
4.令牌由Redis服务端来分配;
再思考一个问题:
如果一个进程实例由于异常给宕掉,那是如何通知令牌管理者(Redis服务器),“我”出了问题,我将把令牌归还给你呢?
可以这样做:
通过给令牌设置TTL(Time To Live),由且只能由MASTER进程在正常状态时,不断刷新令牌的TTL,当MASTER出现异常情况,宕掉之后,无进程刷新令牌TTL,导致令牌过期,将被Redis服务器自动删除“占用标识”。
其余的SLAVER进程不断地、定时地、虎视眈眈地检测是否可对KEY进行关联。
当有个SLAVER进程发现SET请求操作成功返回时,也就是此SLAVER进程获取到了令牌(主用标识),自身状态由SLAVER转为MASTER,于是干起了只有MASTER才能干得活。在其余的SLAVER进程看来,又是SET请求关联失败,继续“虎视眈眈”地不断检测,等着这个新的MASTER进程异常宕掉的那一时刻。
如何实现?
Code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <hiredis.h>
int main(int argc, char **argv)
{
if (argc < 3)
{
printf("Usage: main HOST PORT TOKEN\n");
exit(1);
}
const char *host = argv[1]; // Redis服务器网络地址
int port = atoi(argv[2]); // Redis服务器服务端口
const char *token = argv[3];// 令牌名(Token)
char hostname[64] = "";
gethostname(hostname, sizeof(hostname)-1); // 获取当前进程所在主机的主机名
hostname[sizeof(hostname)-1] = 0;
char value[1024] = ""; // 生成此进程独一无二的身份标识
snprintf(value, sizeof(value), "_%s_%d", hostname, getpid()); // 生成规则:_$HOSTNAME_$PID
redisContext *c = redisConnect(host, port); // 连接Redis服务器
while (1)
{
if (isMaster(c, token, value) == 1)
{
printf("\033[41;37m MASTER! \033[0m\n\n");
}
else
{
printf("\033[41;37m SLAVER! \033[0m\n\n\n");
}
sleep(1);
}
redisFree(c); // 释放与Redis服务器的链接,Never
exit(0);
}
/*--------------------------------------------------------------
* 函数名称:isMaster
* 功能描述:判断是否为Master状态
* 参数说明:c=Redis客户端;key=令牌名;value=令牌持有者信息;
* 返 回 值:0=SLAVER;1=MASTER
* 备 注:
* */
int isMaster(redisContext *c, const char *key, const char *value)
{
int status = 0;
char cmd[1024] = "";
redisReply *reply = NULL;
do
{
// SET $KEY $VALUE EX $SECOND NX
snprintf(cmd, sizeof(cmd), "SET %s %s EX 10 NX", key, value);
reply = redisCommand(c, cmd);
if (
reply->type == REDIS_REPLY_STATUS &&
strcmp(reply->str, "OK") == 0)
{
status = 1;
printf("exec success: %s\n", cmd);
freeReplyObject(reply);
break;
}
freeReplyObject(reply);
reply = NULL;
// GET $KEY
snprintf(cmd, sizeof(cmd), "GET %s", key);
reply = redisCommand(c, cmd);
if (reply->type != REDIS_REPLY_STRING)
{
status = 0;
printf("exec failed: %s\n", cmd);
freeReplyObject(reply);
break;
}
if (strcmp(reply->str, value) != 0)
{
status = 0;
printf("VALUE(%s) NOT EQUAL value*(%s)\n", value, reply->str);
freeReplyObject(reply);
break;
}
printf("VALUE(%s) EQUAL value*(%s)\n", value, reply->str);
freeReplyObject(reply);
reply = NULL;
// EXPIRE $KEY $SECOND
snprintf(cmd, sizeof(cmd), "EXPIRE %s 10", key);
reply = redisCommand(c, cmd);
if (reply->type != REDIS_REPLY_INTEGER)
{
status = 0;
printf("exec failed: %s\n", cmd);
freeReplyObject(reply);
break;
}
if (reply->integer == 1)
{
status = 1;
printf("exec success: %s\n", cmd);
freeReplyObject(reply);
break;
}
freeReplyObject(reply);
reply = NULL;
status = 0;
} while (0);
return status;
}
/*--------------------------------------------------------------
* 函数名称:printReplyType
* 功能描述:输出hiredis-reply的返回类型
* 参数说明:
* 返 回 值:无
* 备 注:
* */
void printReplyType(redisReply *reply)
{
switch (reply->type)
{
case REDIS_REPLY_STATUS:
printf("REDIS_REPLY_STATUS\n");
break;
case REDIS_REPLY_ERROR:
printf("REDIS_REPLY_ERROR\n");
break;
case REDIS_REPLY_INTEGER:
printf("REDIS_REPLY_INTEGER\n");
break;
case REDIS_REPLY_NIL:
printf("REDIS_REPLY_NIL\n");
break;
case REDIS_REPLY_STRING:
printf("REDIS_REPLY_STRING\n");
break;
case REDIS_REPLY_ARRAY:
printf("REDIS_REPLY_ARRAY\n");
break;
default:
printf("UNKNOWN\n");
}
}
编译:
[redis@test1280 ~]$ gcc -o main main.c -lhiredis -L$HOME/redis/lib
我们在同一个主机的三个终端上执行:
[redis@test1280 ~]$ ./main 127.0.0.1 12001 09D9DCC2B50167ECB64ABB5552798289
PS.09D9DCC2B50167ECB64ABB5552798289是我名字英文拼写的md5摘要(_)
可以观察到:
当我人为地宕掉第一个MASTER进程后:
在一段时间内,SLAVER1和SLAVER2都“不知道”MASTER已经宕掉了,因为这个时候MASTER的关联信息还在Redis服务器中。
再等一段时间,由于原MASTER的关联信息TTL超时过期,关联信息将被Redis自动删除,这个时候SLAVER1和SLAVER2,就可以通过SET来检测并争抢到令牌,至于是谁那就不知道咯。
后面的就是往复循环…
对上面代码的一点解释:
1.为什么在SET失败后还要GET下进行比较?
因为每次调用都是无状态的,与之前之后无关的。
也就是说,即使一个进程实例第一次调用,得到了令牌,成为MASTER状态,由这个进程第二次调用,将会SET失败。
能说这个MASTER进程实例丢掉令牌了吗?不能!
应该从令牌中取出唯一标识,和自己的进行比较。如果发现一样,那这个令牌就是自己设置得到的,自己就是MASTER。
这里注意,每个进程实例都是很“温和”的,很“遵守规则”的,大家都可以从KEY中取得MASTER的进程唯一标识,但是没有人去“篡改”为自己的唯一标志,都是循规蹈矩地“抢”,而非“偷偷摸摸”的抢。
2.过期时间设置
我这里用的过期时间是10秒,大家可以依据实际情况进行相应调整。
3.关于令牌(KEY)的设计:
假设你的可执行文件叫做REDISXX,那么你可以设计KEY名字为:
_SYS_REDISXX_ARG
(ARG是进程启动参数)
这样所有以ARG为启动参数的REDISXX,链接到同一个Redis服务器上,就可以实现高可用争抢啦!
4.关于进程唯一标识(VALUE)的设计:
你可以设计为:
_HOSTNAME_WHOAMI_PID
(HOSTNAME为主机名,WHOAMI为账户名)
为啥不设计VALUE为UUID呢?因为没必要,而且没好处。
自定义设计的唯一标志已经可以满足需求,而且易读易懂,相反,用UUID倒是唯一了,但是可读性不好,给后续排查问题造成阻碍。
依据实际情况,实际需求,合理地、灵活地设计KEY以及VALUE,可以获得强大的实用效果!