~欢迎你!第 AmazingCounters.com 位造访者

基于C的 - Redis哨兵模式客户端实现

  最近工作上需要用到内存数据库 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服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

  参考链接:https://www.jianshu.com/p/06ab9daf921d
  
  哨兵模式还有一个特性,为了提高读写效率,redis在集群和哨兵模式的状态下,可以设置成 slave read-Only,在这种模式下,仅主可写,从仅可读。这种情况让redis master实现写高可用。
 

二、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 = '\0';
                //建立当前连接,存入连接列表中
                connList.nodeInfo[connList.size].conn = redisConnect(connList.nodeInfo[connList.size].ip,
                    connList.nodeInfo[connList.size].port);   //建立连接失败,不进行计数,否则后续会有问题
                if (connList.nodeInfo[connList.size].conn == NULL) { 
                    *pAddr = ':';
                    PTLOG_FILE("[%s],%s",connList.nodeInfo[connList.size].ip,strerror(errno));
                    continue ;
                } else if (connList.nodeInfo[connList.size].conn->err) {
                    *pAddr = ':';
                    PTLOG_FILE("redis连接失败:[%s] error %d:%s",connList.nodeInfo[connList.size].ip,
                        connList.nodeInfo[connList.size].conn->err, connList.nodeInfo[connList.size].conn->errstr);
                    if(connList.nodeInfo[connList.size].conn != NULL){                        
                        redisFree(connList.nodeInfo[connList.size].conn);
                    }
                    continue ;
                }
          /* 建立长连接 KeepAlive*/
                redisEnableKeepAlive(connList.nodeInfo[connList.size].conn);
                //连接列表数量++
                connList.size++;
            }
        } //初始化主从机器,公共连接默认为主连接
        conn = connAsMaster();  
        connAsSlaver();        
    }else{     //单机器模式,主从均为同一台机器
    connList.master = -1;
    connList.slaver = -1;
    connList.size=0;
       conn = connAsSingle( 0 );
    return 1;
    }
    //默认连接master连接
    conn = connList.nodeInfo[connList.master].conn;
    return connList.size; 
}
View Code

  将所有连接建立后,需要校验哪一台是主机,哪一台是从机,目前使用的方法是指定一台为专门写的主机,指定一台从机为专门读的从机。

  目前实现方法,根据环境变量配置查找,查找主机从前往后查,查找从机从后往前查,当主机经过多次挂机重启之后,有可能会出现最后一台为主机的情况,该情况会使得读写在同一台机器上。(可优化)

/**********************************************
* redis哨兵模式读写分离,master机器写高可用,slaver不能进行写操作,需要选择主机进行写入值,如果主机参数不为-1,则说明已经经过初始化,并且已经确定主机,直接返回主机连接
**********************************************/
redisContext* connAsMaster(){
    redisReply *reply;
    int i;
    if(connList.master == -1){
        for( i = 0 ; i < connList.size; i++ ){ 
            reply = redisCommand(connList.nodeInfo[i].conn, "set %s %s", "redis_master_key", "1");//测试插入值
            if (reply == NULL || reply->type == REDIS_REPLY_ERROR || connList.nodeInfo[i].conn->err) {
                if (reply != NULL) {
                    freeReplyObject(reply);
                }else { /*redis连接断开情况,返回值为NULL,重连再次执行一次*/ 
                    if(connList.nodeInfo[i].conn!=NULL){
                        redisFree(connList.nodeInfo[i].conn);
                    }  
                    connList.nodeInfo[i].conn = redisConnect(connList.nodeInfo[i].ip,connList.nodeInfo[i].port); 
                    reply = redisCommand(connList.nodeInfo[i].conn, "set %s %s", "redis_master_key", "1");//测试插入值
                    if (!(reply == NULL && reply->type == REDIS_REPLY_ERROR && connList.nodeInfo[i].conn->err)){                
                        freeReplyObject(reply);
                    }
                }
                continue;
            }
            connList.master = i;
            break;
        }
    }    
    if(connList.master == -1){//无可用连接
        PTLOG_FILE("FAIL:[无可用连接]"); 
        return NULL;
    }
    return connList.nodeInfo[connList.master].conn;
}
View Code
/**********************************************
* 连接从机器,通过查询主机写入的值,查询成功则选定为从机,如果从机参数不为-1,则说明已经经过初始化,并确定从机,直接返回从机连接
**********************************************/
redisContext* connAsSlaver(){
    int i;
    redisReply *reply;
    if(connList.slaver == -1){
        for( i = connList.size - 1 ; i >= 0 ; i-- ){ 
            reply = redisCommand(connList.nodeInfo[i].conn, "get redis_master_key ");
            if (reply == NULL || reply->type == REDIS_REPLY_ERROR || connList.nodeInfo[i].conn->err) {
                 if (reply != NULL) {
                    freeReplyObject(reply);
                }else { /*redis连接断开情况,返回值为NULL,重连再次执行一次*/
                    if(connList.nodeInfo[i].conn!=NULL){
                        redisFree(connList.nodeInfo[i].conn);
                    }                    
                    connList.nodeInfo[i].conn = redisConnect(connList.nodeInfo[i].ip,connList.nodeInfo[i].port); 
                    reply = redisCommand(connList.nodeInfo[i].conn, "get redis_master_key ");//测试插入值
                    if (!(reply == NULL && reply->type == REDIS_REPLY_ERROR && connList.nodeInfo[i].conn->err)){                
                        freeReplyObject(reply);
                    }
                }
                continue;
            }
            connList.slaver = i;
            break;
        }
    }
    if(connList.slaver == -1){//无可用连接
        PTLOG_FILE("FAIL:[无可用连接]"); 
        return NULL;
    }
    return connList.nodeInfo[connList.slaver].conn;
}
View Code

 

 

四、复盘反思

  当初赶进度两个礼拜要完成开发测试,包括熟悉jedis的实现方式,时间实在赶就没有去深入思考怎么实现更合适。

  当然上面成功实现了redis集群模式的支持,但是还是有很多可以进行改进的方式。

  简单举个例子:上述实现没有考虑redis的密码模式(虽然是需求上没有提及,没实现也没问题。)说白了,就是没考虑到!是 bug!   ORZ

  

  有个小插曲:测试在测代码的时候,问了我一个问题,他说他之前测试的jar包使用了redis的依赖,在配置文件中需要配置redis的节点并不是redis-server的端口,而是sentinel端口,而我是通过直接连接redis实现的,有什么区别。

  其实这就是我这个实现与jedis客户端的区别了。

  按照jedis客户端的实现,连接确实是配置sentinel,然后需要通过sentinel查询master机器。

    127.0.0.1:26379> SENTINEL get-master-addr-by-name mymaster
    1) "127.0.0.1"
    2) "6379"

  mymaster是在进行集群配置的时候,写在sentinel.conf中的主机名称。通过这个主机名称可以查出主机的ip和port

  然后建立指向主机的连接,通过命令 role 或者 info replication查看当前机器是否为Master,并查看其从节点。从而来建立从节点的连接。

  再进一步,对于高并发查询的场景,可以将从节点进行一个负载均衡,避免大量查询在一个从节点上。(官方数据表示Redis读的速度是110000次/秒,写的速度是81000次/秒。跑~~~

  127.0.0.1:6380> role
  1) "master"
  2) (integer) 73735184
  3) 1) 1) "127.0.0.1"
        2) "6379"
        3) "73735184"
     2) 1) "127.0.0.1"
        2) "6381"
        3) "73735184"
  127.0.0.1:6380> INFO replication
  # Replication
  role:master
  connected_slaves:2
  slave0:ip=127.0.0.1,port=6379,state=online,offset=73735982,lag=1
  slave1:ip=127.0.0.1,port=6381,state=online,offset=73735982,lag=1
  master_repl_offset:73736115
  repl_backlog_active:1
  repl_backlog_size:1048576
  repl_backlog_first_byte_offset:72687540
  repl_backlog_histlen:1048576

  在最开始开发的时候,看到不存在哨兵连接的接口,我甚至认为C语言不支持哨兵侦测,但是经过熟悉了解后,我还是naive了~

  有时间码一个,实现一下(挖坑~

 

总结:其实很多情况并不是无法实现,而是缺乏思考。

  代码千万条,思考第一条,开发不规范,bug码里藏。

  要沉淀每一次的思考,下次代码能写得更好。

posted @ 2020-11-25 01:16  ~HDMaxfun  阅读(959)  评论(5编辑  收藏  举报