redis集群功能是在3.0之后才加入的,客户端的支持非常有限,只有ruby的是开发完成的,python的和java的都在开发中,所以,需要自己开发一个客户端。


然后这个客户端最好要支持负载均衡,所以还是有点工作要去做的。


https://github.com/0xsky/xredis 


这个是网友实现的一个客户端,通过封装hiredis实现,是pool功能,估计是比较好玩的,以后看。



接下来我要实现一个自己的客户端了,模仿Ruby的实现吧。


https://github.com/antirez/redis-rb-cluster



从redis-rb-cluster的README.md 开始研究它是怎么实现的。


Redis-rb-cluster


Redis Cluster client work in progress. It wraps Redis-rb, and eventually should be part of it.


正在编写过程中,是对redis-rb的包装,所以需要redis-rb.


For now the goal is to write a simple (but not too simple) client that works as a reference implementation, and can be used in order to further develop and test Redis Cluster, that is a work in progress itself.



作为redis集群的示例客户端。


Creating a new instance

In order to create a new Redis Cluster instance use:

startup_nodes = [
    {:host => "127.0.0.1", :port => 6379},
    {:host => "127.0.0.1", :port => 6380}
]
max_cached_connections = 2
rc = RedisCluster.new(startup_nodes,max_cached_connections)



看起来使用起来还是非常方便的,设置好必要的参数就可以使用了。


The startup nodes are a list of addresses of Cluster Nodes, for the client to work it is important that at least one address works. Startup nodes are used in order to:


  • Initialize the hash slot -> node cache, using the 

CLUSTER NODES

  • To contact a random node every time we are not able to talk with the right node currently cached for the specified hash slot we are interested in, in the context of the current request.



The startup nodes是集群中的节点就可以了,最少一个。起始节点用来:



     使用CLUSTER NODES命令初始化 hash slot( redis 按照hash slot来分布数据的)



     随机的接入一个节点就好了,在我们不知道应该连接哪一个节点的时候。


The list of nodes provided by the user will be extended once the client will be able to retrieve the cluster configuration.



一旦连接上集群,那么就能根据实际的集群运行情况更新这个列表了。



The second parameter in the object initialization is the maximum number of connections that the client is allowed to cache. Ideally this should be at least equal to the number of nodes you have, in order to avoid closing and reopening TCP sockets. However if you have very large cluster and want to optimize for clients resource saving, it is possible to use a smaller value.



第二个参数是客户端对多可以连接的客户端个数。理想情况下,最少是集群节点的个数,这样可以避免反复的打开关闭tcp连接。(话说redis集群最少需要3个节点,这个示例的参数是2,这不是误导观众吗?)如果集群特别大,为了节省资源,可以把这个数字设置小一点。


Sending commands

Sending commands is very similar to redis-rb:

rc.get("foo")



Currently only a subset of commands are implemented (and in general multi-keys commands are not supported by Redis Cluster), because for every supported command we need a function able to identify the key among the arguments.



只实现了一部分的命令,因为每个命令都需要根据参数来确定key是哪个集群节点,还得计算crc的值什么的。可以理解。


Disclaimer



Both this client and Redis Cluster are a work in progress that is not suitable to be used in production environments.



 不是用在生产环境中,那我怎么办。。。。


好吧,开始实现一个简单的客户端。



https://github.com/antirez/redis-rb-cluster下载源码,开始分析。




等一下,要不偷一下懒,用简单的方式实现一个?试一试。。。


基本方式是,每次如果发送命令不成功的话,都会告诉你因该去哪里,那么根据返回结果,重新连接就好了。虽然这样会有频繁的重新建立连接,得不到充分利用,但是可以试一试。稍加修改也,不关闭之前的连接就行了啊。就这么干了,今天下午先实现一个简单的,明天再写一个更好的。



因为项目中用到的都是list格式的,所以先拿lpush做实验。


1. 确定lpush命令的返回值。


随便连接一个节点,循环的用不同的key往redis里插入值,然后输出结果


"LPUSH: %d %s \n " , reply-> integer , reply-> str) ;


输出为:


LPUSH: 0 MOVED 5465 127.0.0.1:7001


LPUSH: 1 (null)


可以看出,如果插入成果,返回的integer是1,否则为0. 


只要分析reply的str就可以知道向谁发数据了。好处是不用自己计算crc就知道给谁发送了。



2. 好的,开始码代码。


     时间一分一秒过去了,码好了。


inline void RedisPush(redisContext* &c 
  ,  
  char* key 
  ,  
  char* value) 
  
 { 
  
     redisReply * 
  reply 
  ;
 
       
  reply = (redisReply *)redisCommand(c 
  , 
  "LPUSH %s %s" 
  , key 
  , value) 
  ;
 
  
 
       
  if(  
  reply-> 
  integer ==  
  0 )  
  // 如果不是当前节点,那么就得获取他的host:port
 
      { 
  
         
  // 字符串的格式为 MOVED 5465 127.0.0.1:7001
 
           
  char host[ 
  13] 
  ;
 
           
  int port 
  ;
 
           
  char * h = strrchr( 
  reply-> 
  str 
  , 
  ' ') 
  ;  
  //反向查找第一个空格
 
           
  char * p = strrchr( 
  reply-> 
  str 
  , 
  ':') 
  ; 
  // 反向找到第一个冒号
 
  
 
          memset(host 
  ,  
  0x00 
  ,  
  13) 
  ;
 
          strncpy(host 
  ,h+ 
  1 
  , p-h- 
  1) 
  ;
 
          port = atoi(p+ 
  1) 
  ;
 
          redisFree(c) 
  ;
 
          c = RedisClusterConnection(host 
  ,port) 
  ;
 
           
  reply = (redisReply *)redisCommand(c 
  , 
  "LPUSH %s %s" 
  , key 
  , value) 
  ;
 
  
 
      } 
  
     printf( 
  "%s:%d LPUSH: %d %s 
  \n 
  " 
  , c->tcp.host 
  , c->tcp.port 
  ,  
  reply-> 
  integer 
  ,  
  reply-> 
  str) 
  ;
 
      freeReplyObject( 
  reply) 
  ;
}



然后再补充本地使用crc直接确定具体节点的方式。这样就基本实现功能了。那么ruby的代码分析等到码完这个代码再说吧。




实验了一下,这样会产生大量的reconnection,太可怕了,系统里一片timeout的socket连接。


好吧,先改善reconnect的问题。

改善好了,使用map存储



结构定义



typedef std::map<std::string , redisContext* > RedisConnMap ;
typedef std::map<std::string , redisContext* >::iterator  it_RedisConnMap ;


代码实现




RedisConnMap* GetRedisConnMap() 
   
 { 
   
     
   // 其实是可以通过 CLUSTER NODES 命令获取具体信息的,这里先固定在这里吧。
 
   
 
       
   char *host =  
   "127.0.0.1" 
   ;
 
       
   int port = 
   7000 
   ;
 
       
   char key[ 
   32] 
   ;
 
       sprintf(key 
   , 
   "%s:%d" 
   ,host 
   , port) 
   ;
 
       RedisConnMap* rcp =  
   new RedisConnMap() 
   ;
 
       (*rcp)[key] = RedisClusterConnection(host 
   ,port++) 
   ;
 
       sprintf(key 
   , 
   "%s:%d" 
   ,host 
   , port) 
   ;
 
       (*rcp)[key] = RedisClusterConnection(host 
   ,port++) 
   ;
 
       sprintf(key 
   , 
   "%s:%d" 
   ,host 
   , port) 
   ;
 
       (*rcp)[key] = RedisClusterConnection(host 
   ,port) 
   ;
 
       
   return rcp 
   ;
 
   
} 
   


void RedisClusterPush(RedisConnMap* rcp 
   ,  
   char* key 
   ,  
   char* value) 
   
 { 
   

     it_RedisConnMap it = (*rcp).begin() 
   ;
 
       redisContext *rc = it-> 
   second 
   ;
 
       redisReply *reply 
   ;
 
   
 
       reply = (redisReply *)redisCommand(rc 
   , 
   "LPUSH %s %s" 
   , key 
   , value) 
   ;
 
   
 
       
   if( reply-> 
   integer ==  
   0 )  
   // 如果不是当前节点,那么就得获取他的host:port
 
       { 
   
         
   // 字符串的格式为 MOVED 5465 127.0.0.1:7001
 
           
   char host[ 
   13] 
   ;
 
           
   int port 
   ;
 
           
   char * h = strrchr(reply-> 
   str 
   , 
   ' ') 
   ;  
   //反向查找第一个空格
 
           
   char * p = strrchr(reply-> 
   str 
   , 
   ':') 
   ; 
   // 反向找到第一个冒号
 
           memset(host 
   ,  
   0x00 
   ,  
   13) 
   ;
 
           strncpy(host 
   ,h+ 
   1 
   , p-h- 
   1) 
   ;
 
           port = atoi(p+ 
   1) 
   ;
 
           
   char str[ 
   32] 
   ;
 
           sprintf(str 
   , 
   "%s:%d" 
   ,host 
   , port) 
   ;
 
           
   //printf("from %d reconnec to %d for %s\n",rc->tcp.port, port,reply->str);
 
           rc = (*rcp)[str] 
   ;
 
           reply = (redisReply *)redisCommand(rc 
   , 
   "LPUSH %s %s" 
   , key 
   , value) 
   ;
 
   
 
       } 
   
     printf( 
   "%s:%d LPUSH: %d %s 
   \n 
   " 
   , rc-> 
   tcp. 
   host 
   , rc-> 
   tcp. 
   port 
   , reply-> 
   integer 
   , reply-> 
   str) 
   ;
 
       freeReplyObject(reply) 
   ;
} 
   

void RedisTestAgain() 
   
 { 
   
     
   int i 
   ;
 
       
   char key[ 
   50] 
   ;
 
       
   char value[ 
   500] 
   ;
 
       RedisConnMap* rcp = GetRedisConnMap() 
   ;
 
       
   for(i =  
   0 
   ; i< 
   5 
   ; i++) 
   
     { 
   
         sprintf(key 
   ,  
   "listkey:%d" 
   ,i) 
   ;
 
           sprintf(value 
   ,  
   "value:%d: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" 
   ,i) 
   ;
 
           RedisClusterPush(rcp 
   , key 
   , value) 
   ;
 
       } 
   

 }

虽然没有任何负载均衡可言,但是可以用啦。


好滴,继续写本地的客户端。 这次需要在本地计算crc的值,直接选中连接