zoukankan      html  css  js  c++  java
  • 基于C的

      最近工作上需要用到内存数据库 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 = '';
                    //建立当前连接,存入连接列表中
                    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码里藏。

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

  • 相关阅读:
    笔记
    作用域及this指向案例总结
    笔记
    JS 事件(捕获和冒泡 兼容性写法)
    tab标签
    笔记
    JS入门(10.16)
    JS入门(10.15)
    关键渲染路径
    Practice1小学四则运算(改进)
  • 原文地址:https://www.cnblogs.com/HDMaxfun/p/14032501.html
Copyright © 2011-2022 走看看