最近工作上需要用到内存数据库 redis,架构设计使用redis的哨兵模式,也就是集群模式。
因为是用C开发,但是redis所提供的hiredis头文件中并未提供有关集群模式或者哨兵模式调用的方式,前辈说可以参考一下java库中的jedis的实现,然后有了这篇博客。
一、哨兵模式简述
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。
其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
它的主要功能有以下几点
1、通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
2、如果发现某个Redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
3、当哨兵监测到Master宕机,能够从Master的多个Slave中(至少存活一台Slave)选举一个来作为新的Master,其它的Slave节点会将它指定的Master的地址改为被提升为Master的Slave的新地址。
在使用过程中如果只使用一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
二、java客户端实现方式:
因为对java并不是很熟悉,研究了很久jedis的包只是知道java通过配置三个哨兵端口,以及一个链接池实现了主从,但是一直没弄明白jedis是如何进行主从路由的。
后面在一篇描述redis 哨兵模式详解的博客里面看到了对java-redis客户端的原理介绍,详细可以看下面链接,6、7两点。参考链接 : https://www.cnblogs.com/myitnews/p/13732901.html
总结来说,jedis客户端是通过遍历所有的哨兵端口,找到任意一个可以连接上的哨兵,发送请求 get-master-addr-by-name 请求,确认Master节点,然后会向这个 Master节点发送role或info replication 命令,来确定其是否是Master节点,同时获取slave节点的信息,然后存到了java的链接池中。后续使用的时候,就会通过链接池来进行操作了,若master挂掉了,会重复上面操作,重新查询新的Master节点。
三、偷懒了的C语言实现方式:
一开始了解到java客户端的实现方式后,我暂时陷入了一个比较困扰的境地。
用过C链接redis的读者可能知道,redis提供的C语言动态库libhiredis,其中并没有提供直接链接集群模式、哨兵模式的链接池链接方法。libhiredis中提供的方式全部链接方式是与redis直连。所以支持redis的集群模式,至少会需要手动实现一个链接池。
就目前的需求而言,我需要满足哨兵模式的支持,实现程序与redis哨兵模式的交互。
时间有限,从简单的方式先实现需求,我至少需要实现以下几点:
1、redis连接池(暂时不考虑哨兵查询,直接与redis-server建立连接)
2、连接池能够实现主从鉴别(根据主从读写分离进行判断)
3、需要支持高并发场景(建立长连接,避免重复重连影响效率)
建立单独连接节点及连接池,使用的是静态的变量保存的连接池。
//连接节点及相关信息
typedef struct connNode{ char ip[200]; int port; redisContext * conn; }conn_node;
//redis节点池,因为需求使用的是3台redis,一主二从,设置当前最大为八台机器 typedef struct redisNode{ int size; /*总机器数*/ int master; /*主机序号*/ int slaver; /*从机序号* conn_node nodeInfo[8];/*连接节点列表*/ }redisconn;
static redisconn connList; /*静态连接池,保持长连接*/
初始化连接池
/******************************************** * 建立redis链接 * 支持单机版与哨兵模式版本 通过linux环境变量配置 .bash_profile * 单机版本,同一台机器进行读写 * 哨兵模式,一主多从,主机写高可用,从机器只可读 * 通过插入测试值方式对哨兵模式下主机进行甄别,并记录主机 *********************************************/
int redis_init() { char addr[1024]; char *p,*pAddr; int i,len; memset(addr, 0, sizeof(addr)); if (getenv("REDIS_ADDR") == NULL) { PTLOG_FILE("redis节环境变量未配置;【REDIS_ADDR】"); return -1; } snprintf(addr, sizeof(addr), "%s,", getenv("REDIS_ADDR")); /*初始化前需要将redis中的连接对象释放掉,否则不会关闭句柄,也可能内存泄漏*/ for(i=0;i<8;i++){ if(connList.nodeInfo[i].conn != NULL){ redisFree(connList.nodeInfo[i].conn); } } memset(&connList,0,sizeof(connList)); //哨兵模式,读写分离,初始化主从机器均为 -1 connList.master = -1; connList.slaver = -1; connList.size=0; p = strchr(addr, ','); //环境变量,配置逗号分隔,表示多个 if(p != NULL){//多台redis,哨兵模式 p=addr; len = strlen(addr); for(i = 0 ; i< len ; i++){ if(addr[i]==','){ pAddr = addr + i; memset(connList.nodeInfo[connList.size].ip,0,200); snprintf(connList.nodeInfo[connList.size].ip,(pAddr - p + 1),"%s",p); p = addr + i + 1; //查询端口信息 pAddr = strrchr(connList.nodeInfo[connList.size].ip, ':'); if (pAddr == NULL || (connList.nodeInfo[connList.size].port = atoi(pAddr+1)) <= 0) { //端口有误,跳过当前配置,不进行计数 PTLOG_FILE("环境变量 REDIS_ADDR 配置有误:[%s]",connList.nodeInfo[connList.size].ip); continue ; } *pAddr = '