Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。ok,这些大家都知道,接下来我们介绍一个简单的高可用方案,第一次写,从哪说起呢,redis在使用的过程中,会有一个主从的概念,主机用于写入,而从机多用于读取。

     从读取的角度,我们关心的是是否读取成功,如果仅仅有单机,那么当此机挂掉之后我们的程序会显的很脆弱,功能无法正常进行下去,因此需要一个备机的存在,我们就叫他备胎吧,在主机(此时的主机并非真正意义上的主机,只是相对于备胎,避免歧义称之为现任吧)挂掉的时候瞬间撑起读取的大旗,让程序继续为女神服务,那么问题来了,我们如何获知这个现任挂掉了呢?很简单,在真正的拥有写入权限的主机上不停的写入当前的时间戳(此时间戳会因为主从关系同步到从机),每次获取到所谓现任的时候检查一下此时间戳是否更新便可以获知此刻状态,从而切换至存活的读取从机,也就是备胎上任。

       以上方案解决了读取从机的切换,这里有一个较大的问题就是如果写入时间戳的主机挂掉了呢?这里介绍的是一种简单的处理方案,先从sentinel说起,sentinel即哨兵(一种特殊的redis实例),哨兵的主要工作内容就是监控主机,监控的方式可以查阅文档,此处简单介绍,每一个sentinel拥有自己的配置文件,即sentinel.config,可以下载并部署试一试,配置写入(订阅)方式大致如:“sentinel monitor 主机名 主机ip 主机端口 客观下线投票个数”。这里的客观下线是指的是多个sentinel实例对同一个服务器做出了sdown的判断,通过sentinel is-mastet-down-by-addr命令相互交流后得出的判断,还有一个主观下线是指单个sentinel实例对主机作出的下线判断。订阅之后,此客户端就会收到关于主机切换的一切信息,主机切换即原本用于读取的机器在主机挂掉期间承担起主机的责任,使整个集群能够正常工作,切换信息大致如下<master name> <oldip><oldport><newip><newport>,通过收到的信息,可以将时间戳写入新的主机中,并且同步到其它从机中,整个系统得以正常运转。

       原理大致如上,以下简单以代码说明实际的实现方案。(go语言实现)

1 初始化sentinel

//初始化函数
func (sentinel *Sentinel)InitSentinel(ip, port string) (err error){
   sentinel.Host = ip
   sentinel.Port = port
   addr := ip+":"+port
   sentinel.SentinelClient, err = redis.Dial("tcp",addr)
   sentinel.SentinelClientGCF, err = redis.Dial("tcp",addr)
   defer alert.MonitorAlert(&err)
   if err != nil {
      log.Error("InitSentinel init fail with err:", err)
   }
   return err
}



两个客户端的原因是,一个用于订阅,一个用于获取当前主机的ip以及端口号。因为一旦一个sentinel客户端订阅之后便不能再有其它用途。


2 开启订阅

func (sentinel *Sentinel)SubscribeSentinel(processMessage func(message []byte), restartSubscribe func()) {
 
        psc := redis.PubSubConn{Conn: sentinel.SentinelClient}
        go func() {
          for {
              switch n := psc.Receive().(type) {
              case redis.Message:
                 processMessage(n.Data)
              case redis.PMessage:
                 log.Debug("PMessage is %s", n.Data)
              case redis.Subscription:
                 log.Debug("Subsribe channel:%s", n.Channel)
              case error:
                 log.Error("SubscribeSentinel err")
                 restartSubscribe()
              }
           }
        }()

        go func() {
           psc.Subscribe(switchMasterMessage)
        }()
}



订阅函数由两个协程构成,相当于两个线程(更加轻量级),上边的用于接受订阅消息,下边的用于订阅,函数的参数由两个回调函数构成,一个用于处理订阅消息,一个用于重启sentinel,

3初始化一个map,用于存储需要监控的主机群【一个sentinel群可以监控多组主机】

func (client *RedisClient)InitClientMap(){
        client.clientMap = make(map[string]*redis.Conn)
    }



4.添加监控主机的ip port以及name

func (client *RedisClient)AddClient(masterName, ip, port string) (err error){
        addr := ip + ":" + port
        if _, exist := client.clientMap[masterName]; exist{
           log.Error("AddClient already existed")
        }else {
           conn, err := redis.Dial("tcp", addr)
           if err != nil {
              log.Error("AddClient fail err", err)
           }
           client.clientMap[masterName] = &conn
        }
        client.InitCheckStatusClient(addr ,masterName)
        return err
    }



这里有一个checkstatusclient,用作抢占,此服务可作为分布式部署。

5.写入时间戳

func (subscribe *Subscribe)SetTimeStamp() {
       nowTime := time.Now().Unix()
       log.Debug("TaskRedisSaliveHandler now time is %ld", nowTime)
       subscribe.RedisClientP.SetSaliveGoroutine(nowTime)
    }

       这里写入时间戳,因为是监控了多组主机,所以需要异步处理,异步写入时间戳,异步带来了性能生的提升,但是,因为我们写入的是时间戳,可能会出现先后顺序颠倒的情况,那么解决的方案有一个,就是使用golang的管道技术,相当于一个顺序消息对列,这样就可以控制顺序不至于颠倒的囧境,所以构造了一个如下的管道组。

client.channelMap = make(map[string]chan SetInfo, client.MasterCount())



透过如下使用方式来接受管道消息:中间的函数是写入时间戳的具体实现,参数为一个时间戳一个具体的客户端连接。

func (client *RedisClient)ReceiveChannelInfo() {
       for _, elem := range client.channelMap {
          go func() {
             for info := range elem {
                setRedisSalive(info.timeStamp, info.cn)
             }
          }()
       }
   }



写入管道消息的方式如下:

func (client *RedisClient)SetSaliveGoroutine(info int64) {
        for masterName, temp := range client.clientMap {
           go func() {
              var setInfoTemp SetInfo
              setInfoTemp.cn = temp
              setInfoTemp.timeStamp = info
              client.channelMap[masterName] <- setInfoTemp
           }()

        }
}


     6,