Redis:使用SET命令实现简单的高可用(HA)

本文介绍了一种利用Redis实现进程间高可用性的方法。通过设置带有过期时间的唯一令牌,确保任一时刻只有一个进程执行关键任务,其他进程作为备用。一旦主进程失效,备用进程可通过竞争令牌接替工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

##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

尚且“不知情”的SLAVER1

尚且“不知情”的SLAVER2

在一段时间内,SLAVER1和SLAVER2都“不知道”MASTER已经宕掉了,因为这个时候MASTER的关联信息还在Redis服务器中。

再等一段时间,由于原MASTER的关联信息TTL超时过期,关联信息将被Redis自动删除,这个时候SLAVER1和SLAVER2,就可以通过SET来检测并争抢到令牌,至于是谁那就不知道咯。

SLAVER1争抢到令牌,转为MASTER状态

SLAVER2争抢令牌失败,保持SLAVER状态

后面的就是往复循环…

对上面代码的一点解释:

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,可以获得强大的实用效果!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值